Trivial C# Random Exploitation
ATT&CK techniques detected
Which technique(s) should be tagged here? Pick zero or more — leaving blank just records that the original was wrong.
No matches for .
Loading techniques…
Summary
<p>Exploiting random number generators requires math, right? Thanks to C#’s <code class="language-plaintext highlighter-rouge">Random</code>, that is not necessarily the case! I ran into an HTTP 2.0 web service issuing password reset tokens from a custom encoding of <code class="language-plaintext highlighter-rouge">(new Random()).Next(min, max)</code> output. This led to a critical account takeover. Exploitation did not require scripting, math or libraries. Just several clicks in Burp. While I had source code, I will show a method of discovering and exploiting this vulnerability in a black-box or bug-bounty style engagement.</p> <p>The exploit uses no math, but I do like math. So, there is a bonus section on how to optimize and invert <code class="language-plaintext highlighter-rouge">Random</code>.</p> <h2 id="the-vulnerability">The Vulnerability</h2> <p>I can’t share the client code, but it was something like this:</p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">num</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Random</span><span class="p">().</span><span class="nf">Next</span><span class="p">(</span><span class="n">min</span><span class="p">,</span> <span class="n">max</span><span class="p">);</span> <span class="kt">var</span> <span class="n">token</span> <span class="p">=</span> <span class="nf">make_password_reset_token</span><span class="p">(</span><span class="n">num</span><span class="p">);</span> <span class="nf">save_reset_token_to_db</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span> <span class="k">return</span> <span class="nf">issue_password_reset</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">email</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span> </code></pre></div></div> <p>This represents a typical password reset. The token is created using <code class="language-plaintext highlighter-rouge">Random()</code>, and there is no seed. This gets encoded to an alphanumeric token. The token is sent to the user in email. The user can then log in with their email and token.</p> <p>This may be trivially exploitable.</p> <h2 id="how-the-c-prng-works">How the C# PRNG Works</h2> <p>Somehow documentation linked me to the following reference implementation. This is not the real implementation, but it’s good enough. Don’t get into the weeds here, the <code class="language-plaintext highlighter-rouge">Random(int Seed)</code> is only displayed for the sake of context.</p> <p><a href="https://github.com/microsoft/referencesource/blob/f7df9e2399ecd273e90908ac11caf1433e142448/mscorlib/system/random.cs#L52-L82">Git link</a></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">public</span> <span class="nf">Random</span><span class="p">()</span> <span class="p">:</span> <span class="k">this</span><span class="p">(</span><span class="n">Environment</span><span class="p">.</span><span class="n">TickCount</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span> <span class="k">public</span> <span class="nf">Random</span><span class="p">(</span><span class="kt">int</span> <span class="n">Seed</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">ii</span><span class="p">;</span> <span class="kt">int</span> <span class="n">mj</span><span class="p">,</span> <span class="n">mk</span><span class="p">;</span> <span class="c1">//Initialize our Seed array.</span> <span class="c1">//This algorithm comes from Numerical Recipes in C (2nd Ed.)</span> <span class="kt">int</span> <span class="n">subtraction</span> <span class="p">=</span> <span class="p">(</span><span class="n">Seed</span> <span class="p">==</span> <span class="n">Int32</span><span class="p">.</span><span class="n">MinValue</span><span class="p">)</span> <span class="p">?</span> <span class="n">Int32</span><span class="p">.</span><span class="n">MaxValue</span> <span class="p">:</span> <span class="n">Math</span><span class="p">.</span><span class="nf">Abs</span><span class="p">(</span><span class="n">Seed</span><span class="p">);</span> <span class="n">mj</span> <span class="p">=</span> <span class="n">MSEED</span> <span class="p">-</span> <span class="n">subtraction</span><span class="p">;</span> <span class="n">SeedArray</span><span class="p">[</span><span class="m">55</span><span class="p">]=</span><span class="n">mj</span><span class="p">;</span> <span class="c1">// [2]</span> <span class="n">mk</span><span class="p">=</span><span class="m">1</span><span class="p">;</span> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span><span class="p">=</span><span class="m">1</span><span class="p">;</span> <span class="n">i</span><span class="p"><</span><span class="m">55</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> <span class="p">{</span> <span class="c1">//Apparently the range [1..55] is special (Knuth) and so we're wasting the 0'th position.</span> <span class="n">ii</span> <span class="p">=</span> <span class="p">(</span><span class="m">21</span><span class="p">*</span><span class="n">i</span><span class="p">)%</span><span class="m">55</span><span class="p">;</span> <span class="n">SeedArray</span><span class="p">[</span><span class="n">ii</span><span class="p">]=</span><span class="n">mk</span><span class="p">;</span> <span class="n">mk</span> <span class="p">=</span> <span class="n">mj</span> <span class="p">-</span> <span class="n">mk</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">mk</span><span class="p"><</span><span class="m">0</span><span class="p">)</span> <span class="n">mk</span><span class="p">+=</span><span class="n">MBIG</span><span class="p">;</span> <span class="n">mj</span><span class="p">=</span><span class="n">SeedArray</span><span class="p">[</span><span class="n">ii</span><span class="p">];</span> <span class="p">}</span> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">k</span><span class="p">=</span><span class="m">1</span><span class="p">;</span> <span class="n">k</span><span class="p"><</span><span class="m">5</span><span class="p">;</span> <span class="n">k</span><span class="p">++)</span> <span class="p">{</span> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span><span class="p">=</span><span class="m">1</span><span class="p">;</span> <span class="n">i</span><span class="p"><</span><span class="m">56</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> <span class="p">{</span> <span class="n">SeedArray</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">-=</span> <span class="n">SeedArray</span><span class="p">[</span><span class="m">1</span><span class="p">+(</span><span class="n">i</span><span class="p">+</span><span class="m">30</span><span class="p">)%</span><span class="m">55</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span><span class="n">SeedArray</span><span class="p">[</span><span class="n">i</span><span class="p">]<</span><span class="m">0</span><span class="p">)</span> <span class="n">SeedArray</span><span class="p">[</span><span class="n">i</span><span class="p">]+=</span><span class="n">MBIG</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="n">inext</span><span class="p">=</span><span class="m">0</span><span class="p">;</span> <span class="n">inextp</span> <span class="p">=</span> <span class="m">21</span><span class="p">;</span> <span class="n">Seed</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>This whole system hinges on the 32 bit <code class="language-plaintext highlighter-rouge">Seed</code>. This builds the internal state (<code class="language-plaintext highlighter-rouge">SeedArray[55]</code>) with some ugly math. If <code class="language-plaintext highlighter-rouge">Random</code> is initialized without an argument, the <code class="language-plaintext highlighter-rouge">Environment.TickCount</code> is used as <code class="language-plaintext highlighter-rouge">Seed</code>. All output of a PRNG is determined by its seed. In this case, it’s the <a href="https://learn.microsoft.com/en-us/dotnet/api/system.environment.tickcount?view=net-9.0">TickCount</a></p> <ul> <li>essentially just time. So you can think of this whole algorithm as emailing you the time, just with a very odd encoding.</li> </ul> <p>In some sense, you can even submit a time to encode. You do this, not with a URL parameter but by waiting. Wait for the right time and you get the encoding you want. What time, or event, should we wait for?</p> <h2 id="the-exploit">The Exploit</h2> <p>The <a href="https://learn.microsoft.com/en-us/dotnet/api/system.random.-ctor?view=net-9.0">documentation</a> says it best.</p> <blockquote> <p>In .NET Framework, the default seed value is derived from the system clock, which has finite resolution. As a result, different <code class="language-plaintext highlighter-rouge">Random</code> objects that are created in close succession by a call to the parameterless constructor have identical default seed values and, therefore, produce identical sets of random numbers.</p> </blockquote> <p>If we submit two requests in the same 1ms window, we get the same <code class="language-plaintext highlighter-rouge">Seed</code>, same seed same output, same reset token sent to two email addresses. One email we own of course, the other belongs to an admin.</p> <p>How do we hit the 1ms window? We use the <a href="https://portswigger.net/research/the-single-packet-attack-making-remote-race-conditions-local">single packet attack</a>.</p> <p>Will it really work though?</p> <h2 id="blackbox-methodology">Blackbox Methodology</h2> <p>You don’t want to go spamming admins with reset emails before you even verify the vulnerability. So make two accounts on the website that you control. While you can do the attack with one account, it’s prone to false positives. You’re sending two account resets in rapid succession. The second request may write a different reset token to the DB before the email service reads the first, resulting in a false positive.</p> <p>Use Burp’s repeater groups to perform the single packet attack to reset both accounts. Check your email for duplicate tokens. If you fail, go on testing other stuff until the lockout window dies. Then just hit send again, likely you don’t need to worry about keeping a session token alive.</p> <p>Note: Burp displays round trip time in the lower-right corner of Repeater.</p> <p> <center><img align="center" alt="Route Trip Time of request 4 in Race group" src="../../../public/images/csharp_prng_repeater_time.png" style="display: block; margin-left: auto; margin-right: auto;" /></center> </p> <p>Keep an eye on that number. Each request has its own time. For me, it took about 10 requests before I got a duplicate token. That only occurred when the difference in round trip times was 1ms or less.</p> <p>When launching the actual exploit, the only way to check if your token matches the victim account’s, is to log in. Login requests tend to be rate limited and guarded. So first verify with testing accounts. Use that to obtain a delta time window that works. Then, when launching the actual exploit, only attempt to log in when the delta time is within your testing bounds.</p> <p><strong>Ah… I guess subtracting two times counts as math. Exploiting PRNG’s always require math.</strong></p> <h2 id="wrapping-up">Wrapping Up</h2> <p>This attack is not completely novel. I have seen similar attacks used in CTFs. It’s a nice lesson on time though. We control time by waiting, or not waiting. If a secret token is just an encoded time, you can duplicate them by duplicating time.</p> <p>If you look into the .NET runtime enough, you can convince yourself this attack won’t work. <code class="language-plaintext highlighter-rouge">Random</code> has more then one implementation, the one my client should have used <a href="https://github.com/dotnet/runtime/blob/6454b4c4b99c39fa896367a85a1367adc789a7c0/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro128StarStarImpl.cs#L38">does not seed by time</a>. I can even prove this with <a href="https://dotnetfiddle.net/5xlOSj">dotnetfiddle</a>. This is like the security version of “it works on my computer”. This is why we test “secure” code and why we fuzz with random input. So try this exploit next time you see a security token.</p> <p>This applies to more then just C#’s <code class="language-plaintext highlighter-rouge">Random</code>. Consider Python’s <code class="language-plaintext highlighter-rouge">uuid</code>? The <a href="https://docs.python.org/3/library/uuid.html">documentation</a> warns of potential collisions due to lack of “synchronization” depending on “underlying platform”, unless <code class="language-plaintext highlighter-rouge">safeUUID</code> is used. I wonder if the attack will work there? Only one way to find out.</p> <p>The fix for weak PRNG vulnerabilities is always to check the documentation. In this case you have to click the “Supplemental API remarks for Random.” in the “<a href="https://learn.microsoft.com/en-us/dotnet/api/system.random?view=net-9.0#remarks">Remarks</a>” section to get to the <a href="https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-random">security info</a> where it says:</p> <blockquote> <p>To generate a cryptographically secure random number, such as one that’s suitable for creating a random password, use one of the static methods in the <a href="https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.randomnumbergenerator?view=net-9.0">System.Security.Cryptography.RandomNumberGenerator</a> class`.</p> </blockquote> <p>So C# use <code class="language-plaintext highlighter-rouge">RandomNumberGenerator</code> instead of <code class="language-plaintext highlighter-rouge">Random</code>.</p> <h2 id="bonus-cracking-cs-old-random-algorithm">Bonus: Cracking C#’s Old Random Algorithm</h2> <p>Ahead is some math. It’s not too bad, but figured I would warn you. This is the “hard” way to exploit this finding. I wrote a library that can predict the output of <code class="language-plaintext highlighter-rouge">Random::Next</code>. It can also invert it, to go back to the seed. Or you can find the first output from the seventh output. None of this requires brute force, just a single modular equation. The code can be found <a href="https://github.com/doyensec/csharp_rand_py/">here</a>.</p> <p>I intended this to be a fun weekend math project. Things got messed up when I found collisions due to an int underflow.</p> <h3 id="seeding-is-all-just-math">Seeding Is All Just Math</h3> <p>Let’s look at the seed algorithm, but try to generalize what you see. The <code class="language-plaintext highlighter-rouge">SeedArray[55]</code> is obviously the internal state of the PRNG. This is built up with “math”. If you look closely, <em>almost</em> every time <code class="language-plaintext highlighter-rouge">SeedArray[i]</code> is assigned, it’s with a subtraction. Right afterward you always see a check, did the subtraction results in a negative number? If so, add <code class="language-plaintext highlighter-rouge">MBIG</code>. In other words, all the subtraction is done mod <code class="language-plaintext highlighter-rouge">MBIG</code>.</p> <p>The <code class="language-plaintext highlighter-rouge">MBIG</code> value is <code class="language-plaintext highlighter-rouge">Int32.MaxValue</code>, aka 0x7fffffff, aka <code class="language-plaintext highlighter-rouge">2^31 - 1</code>. This is a <a href="https://en.wikipedia.org/wiki/Mersenne_prime">Mersenne prime</a>. Doing math, mod’ing a prime results in what math people call a <a href="https://en.wikipedia.org/wiki/Finite_field">Galois field</a>. We only say that because <a href="https://en.wikipedia.org/wiki/%C3%89variste_Galois">Évariste Galois</a> was so cool. A Galois field is just a nice way of saying “we can do all the normal algebra tricks we learned since middle school, even though this isn’t <em>normal</em> math”.</p> <p>So, lets say <code class="language-plaintext highlighter-rouge">SeedArray[i]</code> is some <code class="language-plaintext highlighter-rouge">a*Seed + b mod MBIG</code>. It gets changed in a loop though by subtracting some other <code class="language-plaintext highlighter-rouge">c*Seed + d mod MBIG</code>. We don’t need that loop - algebra says to just <code class="language-plaintext highlighter-rouge">(a+c)*Seed + (b+d) Mod MBIG</code>. By churning through the loop doing algebra you can get every element of <code class="language-plaintext highlighter-rouge">SeedArray</code> in the form of <code class="language-plaintext highlighter-rouge">a*Seed + b mod MBIG</code></p> <p>Every time the PRNG is sampled, <code class="language-plaintext highlighter-rouge">Random::InternalSample</code> is called. That is just another subtraction. The result is both returned and used to set some element of <code class="language-plaintext highlighter-rouge">SeedArray</code>. It’s just some equation again. It’s still in the Galois field, it’s still just algebra and you can invert all of these equations. Given one output of <code class="language-plaintext highlighter-rouge">Random::Next</code> we can invert the corresponding equation and get the original <code class="language-plaintext highlighter-rouge">Seed</code>.</p> <p>But, we can do more too!</p> <h3 id="csharp_randpy-library">csharp_rand.py library</h3> <p>The library I made builds <code class="language-plaintext highlighter-rouge">SeedArray</code> from these equations. It will output in terms of these equations. Let’s get the equation that represents the first output of <code class="language-plaintext highlighter-rouge">Random</code> for any <code class="language-plaintext highlighter-rouge">Seed</code>:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>>> from csharp_rand import csharp_rand >>> cs = csharp_rand() >>> first = cs.sample_equation(0) >>> print(first) rand = seed * 1121899819 + 1559595546 mod 2147483647 </code></pre></div></div> <p>This represents the first output of <code class="language-plaintext highlighter-rouge">Random</code> for any <code class="language-plaintext highlighter-rouge">seed</code>. Use <code class="language-plaintext highlighter-rouge">.resolve(42)</code> to get the output of <code class="language-plaintext highlighter-rouge">new Random(42).Next()</code>.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>>> first.resolve(42) 1434747710 </code></pre></div></div> <p>Or invert and resolve <code class="language-plaintext highlighter-rouge">1434747710</code> to find out what seed will produce <code class="language-plaintext highlighter-rouge">1434747710</code> for the first output of <code class="language-plaintext highlighter-rouge">Random</code>.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>>> first.invert().resolve(1434747710) 42 </code></pre></div></div> <p>This agrees with (<a href="https://dotnetfiddle.net/8PROFM">dotnetfiddle</a>).</p> <p>See the <a href="https://github.com/doyensec/csharp_rand_py/blob/main/README.md">readme</a> for more complicated examples.</p> <h3 id="an-int-underflow-in-random">An int Underflow in Random</h3> <p>Having just finished my library, I excitedly showed it to the first person who would listen to me. Of course it failed. There must be a bug and of course I blamed the original implementation. But since account takeover bugs don’t care about my feelings, I fixed the code… mostly…</p> <p>In short, the original implementation has an int underflow which throws the math equations off for certain seed values. Only certain <code class="language-plaintext highlighter-rouge">SeedArray</code> elements are affected. For example, the following shows the first output of <code class="language-plaintext highlighter-rouge">Random</code> does not need any adjustment, but 13th output does.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>>> print(cs.sample_equation(0)) rand = seed * 1121899819 + 1559595546 mod 2147483647 >>> print(cs.sample_equation(12)) rand = seed * 1476289907 + 1358625013 mod 2147483647 underflow adjustment: -2 </code></pre></div></div> <p>So the 13th output will be <code class="language-plaintext highlighter-rouge">seed * 1476289907 + 1358625013</code>, unless the seed causes an underflow, then it will be off by <code class="language-plaintext highlighter-rouge">-2</code>. The code attempts to decide if the overflow occurs itself. This works great until you invert things.</p> <p>Consider, what seed value will produce 908112456 as the 13th output of <code class="language-plaintext highlighter-rouge">Random::Next</code>?</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>>> cs.sample_equation(12).invert().resolve2(908112456) (619861032, 161844078) </code></pre></div></div> <p>Both seeds, 619861032 and 161844078, will produce 908112456 on the 13th output (<a href="https://dotnetfiddle.net/LtMifN">poc</a>). Seed 619861032 does it the <em>proper</em> way, from the non-adjusted equation. Seed 619861032 gets there from the underflow. This “collision” means there are exactly 2 seeds that produce the same output. This means 908112456 is 2x more likely to occur on the 13th output then the first. It also means there is no seed that will produce 908112458 on the 13th output of <code class="language-plaintext highlighter-rouge">Random</code>. A quick brute force produced some 80K+ other “collision” just like this one.</p> <h2 id="bonus-conclusion">Bonus Conclusion</h2> <p>Sometimes the smart way is dumb. What started as a fun math thing ended up feeling like <em>death by a thousand cuts</em>. It’s better to version match and language match your exploit and get it going fast. If it takes a long time, start optimizing while it’s still running. But before you optimize, TEST! Test everything! Otherwise you will run a brute force for hours and get nothing. Why? well maybe <code class="language-plaintext highlighter-rouge">Random(Environment.TickCount)</code> is not <code class="language-plaintext highlighter-rouge">Random()</code> because explicitly seeding results in a different algorithm! <a href="https://dotnetfiddle.net/giZM4G">Ugh…</a>. I am going to go audit some more endpoints…</p>