<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://1392081456.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://1392081456.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-06-04T11:28:43+08:00</updated><id>https://1392081456.github.io/feed.xml</id><title type="html">Colorful White — Defensive Security Notes</title><subtitle>Detection engineering, vulnerability research, and adversarial ML from a defender&apos;s point of view. Personal research notebook of Colorful White (Guangdong University of Technology).</subtitle><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><entry><title type="html">Modern Mitigation Bypass Is Leak Chaining</title><link href="https://1392081456.github.io/2026/07/10/modern-mitigation-bypass-is-leak-chaining/" rel="alternate" type="text/html" title="Modern Mitigation Bypass Is Leak Chaining" /><published>2026-07-10T09:30:00+08:00</published><updated>2026-07-10T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/10/modern-mitigation-bypass-is-leak-chaining</id><content type="html" xml:base="https://1392081456.github.io/2026/07/10/modern-mitigation-bypass-is-leak-chaining/"><![CDATA[<p>The last four levels of pwn.college’s Program Security module were the hardest to finish, and they share a single shape. None of them falls to one clever trick. Each one runs the full mitigation stack — stack canary, PIE, NX, RELRO, sometimes SUID privilege separation — and the only way through is to <strong>chain small leaks until every mitigation has been turned back into a known value</strong>.</p>

<p>That is the durable lesson of the closing four (<code class="language-plaintext highlighter-rouge">can-it-fizz</code>, <code class="language-plaintext highlighter-rouge">does-it-buzz</code>, <code class="language-plaintext highlighter-rouge">make-it-fizzbuzz</code>, <code class="language-plaintext highlighter-rouge">latent-leak-hard</code>):</p>

<p><strong>A mitigation is only a defense while the value behind it is unknown. Every byte you leak is a mitigation you remove.</strong></p>

<h2 id="the-workhorse-a-one-byte-partial-overwrite-is-a-leak-primitive">The workhorse: a one-byte partial overwrite is a leak primitive</h2>

<p>Three of the four levels share a FizzBuzz skeleton: a loop that reads input, then <code class="language-plaintext highlighter-rouge">strcpy(dst, src)</code> and prints the result. Because <code class="language-plaintext highlighter-rouge">src</code> lives in the input window, you can repoint it one byte at a time.</p>

<p>The trick is that a single low-byte overwrite never crosses a page boundary, so it can only redirect a pointer to a neighbor in the <em>same</em> page. So you hunt for a useful neighbor:</p>

<ul>
  <li>In <code class="language-plaintext highlighter-rouge">can-it-fizz</code>, the GOT sits on one page but a <code class="language-plaintext highlighter-rouge">stdout</code> copy variable (<code class="language-plaintext highlighter-rouge">R_X86_64_COPY</code>, holding <code class="language-plaintext highlighter-rouge">&amp;_IO_2_1_stdout_</code>) sits on the globals page next to <code class="language-plaintext highlighter-rouge">fuzzbuzz</code>. One byte (<code class="language-plaintext highlighter-rouge">0x18 -&gt; 0x30</code>) repoints <code class="language-plaintext highlighter-rouge">src</code> at it, and the next print leaks a libc address.</li>
  <li>In <code class="language-plaintext highlighter-rouge">does-it-buzz</code>, the same one-byte pivots reach <code class="language-plaintext highlighter-rouge">puts@got</code> (libc) and a self-referential RELATIVE entry (PIE).</li>
  <li>In <code class="language-plaintext highlighter-rouge">make-it-fizzbuzz</code>, the same idea leaks libc, PIE, the stack base, and the canary in four loop iterations.</li>
</ul>

<p>The point is not the specific offsets. The point is that <strong>partial overwrite + same-page pivot is the cheapest leak primitive available</strong>, and it works precisely because PIE only randomizes the high bits you never touch.</p>

<h2 id="turn-the-leak-interface-into-an-arbitrary-write">Turn the leak interface into an arbitrary write</h2>

<p><code class="language-plaintext highlighter-rouge">does-it-buzz</code> shows the next move. If you can control both <code class="language-plaintext highlighter-rouge">src</code> and <code class="language-plaintext highlighter-rouge">dst</code> of the <code class="language-plaintext highlighter-rouge">strcpy</code> in the same iteration, the “print a leak” interface is also an “arbitrary byte-wise write” interface. Read becomes write for free.</p>

<p>From there the canary is irrelevant — the overflow never reaches it — and the solve is pure GOT surgery: stage the address of <code class="language-plaintext highlighter-rouge">win</code>, then a single <code class="language-plaintext highlighter-rouge">strcpy(strcpy@got, scratch)</code> redirects the next call into it. The family name <code class="language-plaintext highlighter-rouge">BabyFizzGOT</code> is the hint; the read primitive and the write primitive are the same code path used twice.</p>

<h2 id="keep-the-loop-alive-long-enough-to-finish-the-chain">Keep the loop alive long enough to finish the chain</h2>

<p>Leak chaining only works if the program stays in the loop while you collect values. <code class="language-plaintext highlighter-rouge">make-it-fizzbuzz</code> adds a subtlety: the loop counter <code class="language-plaintext highlighter-rouge">i</code> sits inside the readable window, and a <code class="language-plaintext highlighter-rouge">%s</code> print stops at the first NUL.</p>

<p>Writing <code class="language-plaintext highlighter-rouge">i = 0xfefefefe</code> solves both problems at once. As a signed integer it is negative, so the loop never reaches its exit condition; and because all four bytes are non-NULL, the <code class="language-plaintext highlighter-rouge">%s</code> print walks straight past the counter into the pointer you actually want to leak. <strong>A counter is not just control flow; under a string-printing leak it is also a NUL barrier you can dissolve.</strong></p>

<h2 id="suid-separation-is-its-own-mitigation--handle-it-explicitly">SUID separation is its own mitigation — handle it explicitly</h2>

<p><code class="language-plaintext highlighter-rouge">can-it-fizz</code> has a quiet trap. A clean <code class="language-plaintext highlighter-rouge">ret2libc</code> into <code class="language-plaintext highlighter-rouge">system("/bin/sh")</code> runs a shell, but the shell has dropped privileges, so <code class="language-plaintext highlighter-rouge">/flag</code> stays unreadable.</p>

<p>The binary is SUID root, which means <code class="language-plaintext highlighter-rouge">euid = 0</code> but <code class="language-plaintext highlighter-rouge">ruid != 0</code>, and <code class="language-plaintext highlighter-rouge">system</code> runs <code class="language-plaintext highlighter-rouge">/bin/sh</code> which drops to the real uid. The fix is to treat privilege separation as a mitigation to undo before the payoff:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setuid(0)            // collapse ruid to 0 while euid is still 0
system("/bin/sh")    // now the shell keeps root
</code></pre></div></div>

<p>One extra gadget in the ROP chain, but skipping it is the difference between a shell and a useless shell. (Watch stack alignment too: with <code class="language-plaintext highlighter-rouge">A = rbp+8</code>, the chain is already 16-byte aligned and needs no extra <code class="language-plaintext highlighter-rouge">ret</code>.)</p>

<h2 id="nx-is-a-placement-problem-not-a-wall">NX is a placement problem, not a wall</h2>

<p><code class="language-plaintext highlighter-rouge">make-it-fizzbuzz</code> ships a never-called <code class="language-plaintext highlighter-rouge">mprotect_stack()</code> that flips its own stack page to RWX. NX says “no code on the stack”; this gadget says “unless I ask nicely.” The chain becomes leak everything, pre-copy shellcode via <code class="language-plaintext highlighter-rouge">strcpy</code> to a high stack slot, then <code class="language-plaintext highlighter-rouge">ret -&gt; mprotect_stack -&gt; shellcode</code>.</p>

<p>The non-obvious failure mode — the same one the constrained-shellcode levels teach — is staging. <code class="language-plaintext highlighter-rouge">mprotect_stack</code>’s own frame executes below <code class="language-plaintext highlighter-rouge">rbp</code> and clears the low part of the buffer, so shellcode placed at <code class="language-plaintext highlighter-rouge">buffer[0]</code> is destroyed by the very call that makes the stack executable. The fix is to place the payload at <code class="language-plaintext highlighter-rouge">B+0x80</code>: past the region the setup code zeroes, but still inside the page that becomes RWX. <strong>Code execution is not enough; the code has to survive the transition into executability.</strong></p>

<h2 id="the-residual-canary-and-a-verification-lesson-worth-keeping">The residual canary, and a verification lesson worth keeping</h2>

<p><code class="language-plaintext highlighter-rouge">latent-leak-hard</code> is the cleanest leak-chaining puzzle, and it almost beat me with a bad measurement.</p>

<p>The output format is <code class="language-plaintext highlighter-rouge">%.318s</code>, which caps at buffer offset <code class="language-plaintext highlighter-rouge">0x13e</code>, while the canary is at <code class="language-plaintext highlighter-rouge">0x148</code> — so the live frame’s canary is <em>never</em> printable. It is not a fork service either, so each connection is a fresh canary and byte-by-byte brute force is out. The canary must be leaked within one connection.</p>

<p>The recursion provides it. The child frame’s buffer overlaps the deep stack the parent’s libc calls (<code class="language-plaintext highlighter-rouge">scanf</code>, <code class="language-plaintext highlighter-rouge">printf</code>) used, and those functions leave a canary copy in their own frames. On the remote <code class="language-plaintext highlighter-rouge">libc-2.31</code>, a copy lands at child buffer offset <code class="language-plaintext highlighter-rouge">0x38</code>. Fill <code class="language-plaintext highlighter-rouge">0x39</code> to overwrite its leading NUL and <code class="language-plaintext highlighter-rouge">%s</code> prints <code class="language-plaintext highlighter-rouge">canary[1..7]</code>.</p>

<p>The lesson is in how I confirmed it. My first verifier brute-forced the offset and judged success by “no smash message + SIGSEGV = wrong canary.” That criterion is <strong>unfalsifiable</strong>: <code class="language-plaintext highlighter-rouge">__stack_chk_fail</code>’s abort path itself crashes with a SIGSEGV, so a <em>correct</em> canary that fails later looks identical to a wrong one. Sixty-four parallel attempts, zero hits, and I nearly concluded <code class="language-plaintext highlighter-rouge">0x38</code> was not the canary.</p>

<p>The fix was a positive criterion: send <code class="language-plaintext highlighter-rouge">'A'*0x148 + candidate</code> — an overflow that touches <em>only</em> the canary and never the return address. If the candidate is right, the frame returns cleanly (<code class="language-plaintext highlighter-rouge">Goodbye!</code>, no crash). That test isolates the one variable and immediately confirmed <code class="language-plaintext highlighter-rouge">0x38</code>. (My local newer libc had no such residual, which is why local GDB never found it — <strong>a leak can be libc-version-specific, so scan the target’s environment, not your own.</strong>)</p>

<p>PIE then needs no full leak: at recursion depth &gt;= 1 the saved RIP is a fixed <code class="language-plaintext highlighter-rouge">challenge</code> return site, so a two-byte partial overwrite to <code class="language-plaintext highlighter-rouge">0x?dc9</code> brute-forces a single nibble (1/16) to hit <code class="language-plaintext highlighter-rouge">win_authed+0x1c</code>.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>The closing four map to recurring real-world mistakes:</p>

<ul>
  <li>copy-relocation and GOT entries adjacent to attacker-influenced pointers, where a one-byte nudge becomes an infoleak;</li>
  <li>the same code path serving as both a disclosure and a write primitive (<code class="language-plaintext highlighter-rouge">strcpy</code> with attacker-controlled src and dst);</li>
  <li>SUID binaries that call <code class="language-plaintext highlighter-rouge">system</code>/<code class="language-plaintext highlighter-rouge">exec</code> without reasoning about ruid/euid;</li>
  <li>self-modifying permission gadgets (<code class="language-plaintext highlighter-rouge">mprotect</code> to RWX) reachable from a corrupted return address;</li>
  <li>secrets left in reused stack regions by library calls, with leak behavior that varies across libc builds.</li>
</ul>

<p>For detection, the high-value signals are: a SUID-root process spawning an interactive shell after <code class="language-plaintext highlighter-rouge">setuid(0)</code>; <code class="language-plaintext highlighter-rouge">mprotect(..., PROT_EXEC)</code> on stack/anon pages followed immediately by execution there; and crash clustering on <code class="language-plaintext highlighter-rouge">__stack_chk_fail</code> consistent with canary brute forcing.</p>

<p>The capstone takeaway for the whole Program Security module:</p>

<p><strong>Mitigations defend unknowns. Exploitation is the work of making each unknown known — one partial-overwrite leak at a time — until the protected control transfer is just arithmetic.</strong></p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="pwn" /><category term="methodology" /><category term="pwn-college" /><category term="ret2libc" /><category term="got-overwrite" /><category term="aslr" /><category term="canary" /><category term="libc" /><summary type="html"><![CDATA[The last four levels of pwn.college’s Program Security module were the hardest to finish, and they share a single shape. None of them falls to one clever trick. Each one runs the full mitigation stack — stack canary, PIE, NX, RELRO, sometimes SUID privilege separation — and the only way through is to chain small leaks until every mitigation has been turned back into a known value.]]></summary></entry><entry><title type="html">Constrained Shellcode Is Interface Design</title><link href="https://1392081456.github.io/2026/07/09/constrained-shellcode-is-interface-design/" rel="alternate" type="text/html" title="Constrained Shellcode Is Interface Design" /><published>2026-07-09T09:30:00+08:00</published><updated>2026-07-09T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/09/constrained-shellcode-is-interface-design</id><content type="html" xml:base="https://1392081456.github.io/2026/07/09/constrained-shellcode-is-interface-design/"><![CDATA[<p>The most useful lesson from the later pwn.college Program Security levels is that tiny shellcode is rarely a byte-golf problem alone.</p>

<p>It is an interface-design problem.</p>

<p>The real question is not:</p>

<p><strong>How do I fit <code class="language-plaintext highlighter-rouge">open/read/write("/flag")</code> into this payload?</strong></p>

<p>It is:</p>

<p><strong>What is the smallest interaction with the environment that still causes the flag to become readable?</strong></p>

<h2 id="reduce-the-objective-before-reducing-the-bytes">Reduce the objective before reducing the bytes</h2>

<p>The cleanest example is the pair of short-budget levels.</p>

<p>If the challenge is launched from a shell you control, you do not need shellcode that opens <code class="language-plaintext highlighter-rouge">/flag</code>, allocates buffers, and writes output. You can prepare the environment first:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ln -sf /flag f
</code></pre></div></div>

<p>Now the payload only needs to make <code class="language-plaintext highlighter-rouge">f</code> readable:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod("f", 4)
</code></pre></div></div>

<p>That reframing collapses the problem into a 12-byte syscall stub. The shell outside the binary does the rest with <code class="language-plaintext highlighter-rouge">cat f</code>.</p>

<p>The payload is small because the <em>interface contract</em> is small.</p>

<h2 id="filters-do-not-remove-primitives-they-change-where-construction-happens">Filters do not remove primitives; they change where construction happens</h2>

<p>Two common shellcode filters look stronger than they are:</p>

<ul>
  <li>forbidden bytes like <code class="language-plaintext highlighter-rouge">0x48</code>;</li>
  <li>forbidden syscall encodings like <code class="language-plaintext highlighter-rouge">0f 05</code>.</li>
</ul>

<p>These are not semantic defenses. They are serialization constraints.</p>

<p>If <code class="language-plaintext highlighter-rouge">0x48</code> is banned, the fix is to stop encoding <code class="language-plaintext highlighter-rouge">mov rdi, rsp</code> in the obvious way and use a stack transfer sequence such as:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>push rsp
pop rdi
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">0f 05</code> is banned, the syscall primitive still exists. You just move the construction to runtime: place <code class="language-plaintext highlighter-rouge">0e 05</code> in memory, increment one byte, and jump into the repaired opcode.</p>

<p>This pattern generalizes beyond shellcode. A filter that bans a literal representation often leaves the underlying primitive intact.</p>

<h2 id="memory-permissions-create-placement-problems-not-just-execution-problems">Memory permissions create placement problems, not just execution problems</h2>

<p>The harder levels stop being about a single payload buffer.</p>

<p>Once a challenge maps one page <code class="language-plaintext highlighter-rouge">RX</code> and another <code class="language-plaintext highlighter-rouge">RWX</code>, or mutates the original input page before control reaches it, “where should the code live?” becomes the main design question.</p>

<p>The right mental model is:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>stage location
-&gt; writable before launch?
-&gt; executable at handoff?
-&gt; preserved after setup code runs?
</code></pre></div></div>

<p>That is why two-page layouts and mprotect-based return chains matter. A payload can fail even after obtaining code execution if it is stored in the same region that setup code later clears or re-protects.</p>

<h2 id="uniqueness-constraints-turn-shellcode-into-staging">Uniqueness constraints turn shellcode into staging</h2>

<p><code class="language-plaintext highlighter-rouge">diverse-delivery</code> shows another kind of interface problem: every byte must be unique.</p>

<p>At that point, direct “final payload” thinking is usually wrong. The realistic path is:</p>

<ol>
  <li>spend the unique bytes on a tiny decoder, copier, or branch gadget;</li>
  <li>let that stage build the real code elsewhere;</li>
  <li>execute the generated second stage under looser conditions.</li>
</ol>

<p>The same idea appears in six-byte limits like <code class="language-plaintext highlighter-rouge">micro-menace</code>. When the budget is too small for useful work, the only credible design is a control-transfer seed, not a full solution.</p>

<h2 id="earlier-memory-corruption-levels-teach-the-same-lesson">Earlier memory-corruption levels teach the same lesson</h2>

<p>The memory-corruption half of the module prepares this mindset.</p>

<p>Those earlier levels are also about interface boundaries:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">%s</code> is not just output; it is a memory disclosure interface if NUL termination is broken.</li>
  <li>a stack canary is not a blocker if you can preserve the contract and write it back unchanged.</li>
  <li>a fork server is not just concurrency; it is a stable oracle interface because children inherit the same secrets.</li>
  <li>a return-address overwrite is useless if the function exits early; the real interface is the guard that decides whether <code class="language-plaintext highlighter-rouge">ret</code> is even reachable.</li>
</ul>

<p>The shellcode levels are the same game with stricter constraints. The byte stream is only one boundary among many.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>These challenge patterns map cleanly to real engineering mistakes:</p>

<ul>
  <li>brittle byte filters that treat syntax as security control;</li>
  <li>RWX or permission-flipping code paths with unsafe staging assumptions;</li>
  <li>helper routines that expose secrets because buffers are reused or unterminated;</li>
  <li>“one dangerous primitive but heavily constrained” designs that remain exploitable because the attacker can reshape the surrounding environment.</li>
</ul>

<p>The durable lesson is simple:</p>

<p><strong>Exploit design starts by minimizing the contract with the target, not by maximizing gadget complexity.</strong></p>

<p>When the contract becomes small enough, even a heavily constrained input surface can be enough.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="pwn" /><category term="methodology" /><category term="pwn-college" /><category term="shellcode" /><category term="memory-corruption" /><category term="aslr" /><category term="canary" /><summary type="html"><![CDATA[The most useful lesson from the later pwn.college Program Security levels is that tiny shellcode is rarely a byte-golf problem alone.]]></summary></entry><entry><title type="html">Reverse Engineering Is Model Recovery</title><link href="https://1392081456.github.io/2026/07/08/reverse-engineering-is-model-recovery/" rel="alternate" type="text/html" title="Reverse Engineering Is Model Recovery" /><published>2026-07-08T09:30:00+08:00</published><updated>2026-07-08T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/08/reverse-engineering-is-model-recovery</id><content type="html" xml:base="https://1392081456.github.io/2026/07/08/reverse-engineering-is-model-recovery/"><![CDATA[<p>The pwn.college Program Security reverse-engineering module is a clean progression from strings to models.</p>

<p>The same question keeps repeating:</p>

<p><strong>What model does this program use to decide that my input is correct?</strong></p>

<p>Sometimes the model is a byte transform. Sometimes it is a branch. Sometimes it is a virtual machine. Sometimes it is a file format plus a constraint solver.</p>

<h2 id="start-with-the-verifier">Start with the verifier</h2>

<p>The early crackmes are small enough to solve by reading constants, but that is not the habit worth keeping.</p>

<p>The useful habit is to write the verifier as:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input -&gt; transform -&gt; comparison -&gt; decision
</code></pre></div></div>

<p>If the transform is reversible, invert it. If it includes a sort, stop looking for a unique original order. A sort validates a multiset, not a string.</p>

<p>This matters because many wrong reverse-engineering attempts are over-specific. They try to recover <em>the</em> intended input when the binary only checks an equivalence class.</p>

<h2 id="patch-the-decision-not-the-program">Patch the decision, not the program</h2>

<p>The patching levels look like a license to change code freely. They are really a lesson in minimality.</p>

<p>When the check is:</p>

<pre><code class="language-asm">call memcmp
test eax, eax
jne fail
</code></pre>

<p>one byte can invert the decision. But the integrity-check variants punish that move. A better patch changes the comparison length:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>memcmp(a, b, 0) == 0
</code></pre></div></div>

<p>That is a smaller semantic change. It preserves the surrounding control flow and passes both the integrity comparison and the final license comparison.</p>

<h2 id="a-vm-is-just-another-model">A VM is just another model</h2>

<p>Yan85 turns the verifier into a custom machine. The trap is thinking that VM reversing is a new category of magic.</p>

<p>It is still model recovery:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3-byte instruction -&gt; register state -&gt; memory state -&gt; syscall state
</code></pre></div></div>

<p>The easy variants print traces. Use them to build the semantic table: opcodes, registers, flag bits, and syscall masks. The hard variants remove the trace, but the bytecode still has structure.</p>

<p>The workflow that held up:</p>

<ul>
  <li>extract the yancode section;</li>
  <li>disassemble in 3-byte chunks;</li>
  <li>implement <code class="language-plaintext highlighter-rouge">IMM</code>, <code class="language-plaintext highlighter-rouge">ADD</code>, <code class="language-plaintext highlighter-rouge">STK</code>, <code class="language-plaintext highlighter-rouge">STM</code>, <code class="language-plaintext highlighter-rouge">LDM</code>, <code class="language-plaintext highlighter-rouge">CMP</code>, <code class="language-plaintext highlighter-rouge">JMP</code>, and <code class="language-plaintext highlighter-rouge">SYS</code>;</li>
  <li>print every compare and syscall;</li>
  <li>solve constraints or assemble VM shellcode.</li>
</ul>

<p>The later levels flip the task. Instead of understanding their yancode, you write yours:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>write "/flag\0" into VM memory
open(path)
read(fd, buf, size)
write(1, buf, n)
exit()
</code></pre></div></div>

<p>Yansanity adds randomized VM mappings. That kills hardcoded practice-mode bytecode, but not the method. Recover the current mapping, then assemble for that mapping.</p>

<h2 id="file-formats-beat-guessing">File formats beat guessing</h2>

<p>The Cows and Bulls levels look interactive. They are not.</p>

<p><code class="language-plaintext highlighter-rouge">gamefile.bin</code> starts with a <code class="language-plaintext highlighter-rouge">CBGF</code> magic and contains the round data. The right interface is not the program prompt. The right interface is the file parser.</p>

<p>The progression is:</p>

<ul>
  <li>parse the header and records;</li>
  <li>solve Bulls/Cows constraints offline;</li>
  <li>reproduce the migration or ordering state;</li>
  <li>for hash variants, enumerate fixed-length guesses and compare SHA256;</li>
  <li>for salted variants, recover the salt placement before hashing.</li>
</ul>

<p>Once the file format is the model, the terminal prompt is only an output adapter.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Reverse engineering writeups often end at “here is the key.” That misses the durable part.</p>

<p>For defenders and analysts, the useful artifacts are:</p>

<ul>
  <li>the transform model;</li>
  <li>the decision point;</li>
  <li>the VM instruction semantics;</li>
  <li>the syscall surface exposed by an interpreter;</li>
  <li>the file format schema;</li>
  <li>the observable failure and success channels.</li>
</ul>

<p>Those artifacts survive challenge-specific flags. They are also the parts that transfer to malware loaders, protected installers, game anti-cheat components, custom protocol parsers, and fragile validation code.</p>

<p>Reverse engineering is model recovery. Once the model is explicit, the rest of the work becomes ordinary engineering: invert it, patch it, emulate it, generate code for it, or solve constraints against it.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="reverse-engineering" /><category term="methodology" /><category term="pwn-college" /><category term="yan85" /><category term="virtual-machines" /><category term="patching" /><category term="file-formats" /><summary type="html"><![CDATA[The pwn.college Program Security reverse-engineering module is a clean progression from strings to models.]]></summary></entry><entry><title type="html">API Testing Is Contract Drift Hunting</title><link href="https://1392081456.github.io/2026/07/07/api-testing-is-contract-drift-hunting/" rel="alternate" type="text/html" title="API Testing Is Contract Drift Hunting" /><published>2026-07-07T09:30:00+08:00</published><updated>2026-07-07T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/07/api-testing-is-contract-drift-hunting</id><content type="html" xml:base="https://1392081456.github.io/2026/07/07/api-testing-is-contract-drift-hunting/"><![CDATA[<p>API bugs often come from contract drift. The frontend shows one contract; the backend accepts a wider one.</p>

<p>The PortSwigger API Testing labs cover the usual drift points: documentation, methods, hidden fields, query strings, and REST paths.</p>

<h2 id="start-from-the-real-traffic">Start from the real traffic</h2>

<p>A visible request such as:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">PATCH /api/user/wiener
</span></code></pre></div></div>

<p>is a map. Walk upward to <code class="language-plaintext highlighter-rouge">/api/user</code>, then <code class="language-plaintext highlighter-rouge">/api</code>. Try documentation paths. If interactive docs are exposed to ordinary users, the hidden contract may include administrative operations.</p>

<h2 id="methods-are-part-of-authorization">Methods are part of authorization</h2>

<p>A product page may only issue:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /api/products/1/price
</span></code></pre></div></div>

<p>but <code class="language-plaintext highlighter-rouge">OPTIONS</code> can reveal:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET, PATCH
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">PATCH</code> accepts:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"price"</span><span class="p">:</span><span class="mi">0</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>then the frontend hid an update method that the backend failed to authorize.</p>

<h2 id="response-fields-are-not-write-fields">Response fields are not write fields</h2>

<p>Mass assignment starts with a diff. If GET returns:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"chosen_discount"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"percentage"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>and POST does not normally send it, test whether POST accepts it anyway. A 100% discount should be server-controlled, not a client-supplied field.</p>

<h2 id="internal-url-building-is-a-parser-boundary">Internal URL building is a parser boundary</h2>

<p>Server-side parameter pollution appears when input is concatenated into an internal request:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>username=administrator%26field=reset_token%23
</code></pre></div></div>

<p>For REST paths, the same issue becomes path pollution:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>username=../../v1/users/administrator/field/passwordResetToken%23
</code></pre></div></div>

<p>The bug is not the encoded character itself. The bug is building internal URLs by string concatenation and letting user input become query or path syntax.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Hardening:</p>

<ul>
  <li>authenticate documentation and OpenAPI endpoints;</li>
  <li>authorize every HTTP method independently;</li>
  <li>use structured URL builders for internal requests;</li>
  <li>normalize and allowlist path segments;</li>
  <li>use write DTOs instead of binding request JSON to domain objects;</li>
  <li>keep price, discount, role, and token fields server-owned;</li>
  <li>make error messages useful for operators, not endpoint discovery.</li>
</ul>

<p>Detection:</p>

<ul>
  <li>user traffic to docs and API definition files;</li>
  <li>encoded separators in username or id parameters;</li>
  <li>unexpected <code class="language-plaintext highlighter-rouge">OPTIONS</code>, <code class="language-plaintext highlighter-rouge">PATCH</code>, <code class="language-plaintext highlighter-rouge">PUT</code>, or <code class="language-plaintext highlighter-rouge">DELETE</code>;</li>
  <li>clients submitting response-only fields;</li>
  <li>reset flows probing arbitrary field names;</li>
  <li>traversal sequences inside API path parameters.</li>
</ul>

<p>API testing is contract drift hunting: find what the backend really accepts, then decide whether the current user should ever have been allowed to send it.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="web-security" /><category term="methodology" /><category term="detection-engineering" /><category term="api-security" /><category term="mass-assignment" /><category term="parameter-pollution" /><category term="portswigger" /><summary type="html"><![CDATA[API bugs often come from contract drift. The frontend shows one contract; the backend accepts a wider one.]]></summary></entry><entry><title type="html">NoSQL Injection Is Query Shape Injection</title><link href="https://1392081456.github.io/2026/07/06/nosql-injection-is-query-shape-injection/" rel="alternate" type="text/html" title="NoSQL Injection Is Query Shape Injection" /><published>2026-07-06T09:30:00+08:00</published><updated>2026-07-06T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/06/nosql-injection-is-query-shape-injection</id><content type="html" xml:base="https://1392081456.github.io/2026/07/06/nosql-injection-is-query-shape-injection/"><![CDATA[<p>NoSQL injection is not just “SQL injection but without SQL.” The common failure is that user input changes the shape of the query.</p>

<p>In the PortSwigger NoSQL labs, a string becomes JavaScript, a scalar becomes a MongoDB operator, and hidden document fields become enumerable through <code class="language-plaintext highlighter-rouge">$where</code>.</p>

<h2 id="strings-become-expressions">Strings become expressions</h2>

<p>A category filter should treat input as data. If it is interpolated into a JavaScript-style condition, syntax and boolean probes reveal the boundary:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Gifts' &amp;&amp; 0 &amp;&amp; 'x
Gifts' &amp;&amp; 1 &amp;&amp; 'x
</code></pre></div></div>

<p>An always-true expression widens the result set:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Gifts'||1||'
</code></pre></div></div>

<p>That is not a category value anymore. It is query logic.</p>

<h2 id="scalars-become-operators">Scalars become operators</h2>

<p>JSON APIs make operator injection easy to miss. The client is expected to send:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"username"</span><span class="p">:</span><span class="s2">"wiener"</span><span class="p">,</span><span class="nl">"password"</span><span class="p">:</span><span class="s2">"peter"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>but the server accepts:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"username"</span><span class="p">:{</span><span class="nl">"$regex"</span><span class="p">:</span><span class="s2">"admin.*"</span><span class="p">},</span><span class="nl">"password"</span><span class="p">:{</span><span class="nl">"$ne"</span><span class="p">:</span><span class="s2">""</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>

<p>The field that should be a string becomes a query object. Schema validation should reject that before the database driver sees it.</p>

<h2 id="where-turns-documents-into-an-oracle"><code class="language-plaintext highlighter-rouge">$where</code> turns documents into an oracle</h2>

<p>If <code class="language-plaintext highlighter-rouge">$where</code> is accepted, JavaScript can inspect the current document:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"$where"</span><span class="p">:</span><span class="s2">"this.password[0]=='a'"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Response differences become a boolean oracle for passwords, reset tokens, and even unknown field names:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"$where"</span><span class="p">:</span><span class="s2">"Object.keys(this)[1].match('^.{0}u.*')"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The database is no longer just filtering rows. It is evaluating attacker-controlled code over user objects.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Hardening:</p>

<ul>
  <li>enforce JSON schemas at the API boundary;</li>
  <li>reject objects where scalar strings are expected;</li>
  <li>deny operator keys in user-controlled input;</li>
  <li>construct query objects from allowlisted fields;</li>
  <li>disable <code class="language-plaintext highlighter-rouge">$where</code> and server-side JavaScript;</li>
  <li>keep reset tokens and credential fields out of user-facing query surfaces;</li>
  <li>normalize login and reset error messages.</li>
</ul>

<p>Detection:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">$ne</code>, <code class="language-plaintext highlighter-rouge">$regex</code>, <code class="language-plaintext highlighter-rouge">$where</code>, <code class="language-plaintext highlighter-rouge">Object.keys(this)</code>, or <code class="language-plaintext highlighter-rouge">this.password</code>;</li>
  <li>quote-plus-boolean probes in query strings;</li>
  <li>login parameters changing type from string to object;</li>
  <li>character-position enumeration patterns;</li>
  <li>reset endpoints probed with unusual token field names.</li>
</ul>

<p>The boundary is the query shape. Once the attacker controls that shape, the database becomes an execution and enumeration engine.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="web-security" /><category term="methodology" /><category term="detection-engineering" /><category term="nosql-injection" /><category term="mongodb" /><category term="authentication" /><category term="portswigger" /><summary type="html"><![CDATA[NoSQL injection is not just “SQL injection but without SQL.” The common failure is that user input changes the shape of the query.]]></summary></entry><entry><title type="html">Race Conditions Are State Transition Bugs</title><link href="https://1392081456.github.io/2026/07/05/race-conditions-are-state-transition-bugs/" rel="alternate" type="text/html" title="Race Conditions Are State Transition Bugs" /><published>2026-07-05T09:30:00+08:00</published><updated>2026-07-05T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/05/race-conditions-are-state-transition-bugs</id><content type="html" xml:base="https://1392081456.github.io/2026/07/05/race-conditions-are-state-transition-bugs/"><![CDATA[<p>Race conditions are often described as “send requests at the same time.” That is the delivery technique, not the bug.</p>

<p>The bug is a non-atomic state transition. The application checks one state, acts later, and leaves a window where another request can change what the first request thought it had validated.</p>

<h2 id="find-the-transition-first">Find the transition first</h2>

<p>The PortSwigger race-condition labs cover the usual targets:</p>

<ul>
  <li>coupon use;</li>
  <li>login failure counters;</li>
  <li>checkout validation;</li>
  <li>pending email changes;</li>
  <li>password-reset tokens;</li>
  <li>registration confirmation.</li>
</ul>

<p>Each one has a state transition. The useful test is to split it into check, write, confirmation, and side effect. The race window usually lives between two of those steps.</p>

<h2 id="same-endpoint-different-endpoint-or-side-effect">Same endpoint, different endpoint, or side effect</h2>

<p>Some races repeat one request:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /cart/coupon
</code></pre></div></div>

<p>Others line up different requests:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /cart
POST /cart/checkout
</code></pre></div></div>

<p>The email-change lab shows a third form: the bug is not the endpoint itself, but an asynchronous side effect. The database says one pending email while the mail-rendering task reads another.</p>

<p>That distinction matters for testing. Replaying one request is not enough; you have to understand the workflow.</p>

<h2 id="precision-beats-volume">Precision beats volume</h2>

<p>Volume helps only if requests actually hit the window. HTTP/2 single-packet attacks and gate-based release make timing sharper:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">payload</span> <span class="ow">in</span> <span class="n">payloads</span><span class="p">:</span>
    <span class="n">engine</span><span class="p">.</span><span class="n">queue</span><span class="p">(</span><span class="n">target</span><span class="p">.</span><span class="n">req</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">gate</span><span class="o">=</span><span class="s">'1'</span><span class="p">)</span>
<span class="n">engine</span><span class="p">.</span><span class="n">openGate</span><span class="p">(</span><span class="s">'1'</span><span class="p">)</span>
</code></pre></div></div>

<p>The goal is not just “many requests.” It is “the server observes the same stale state many times.”</p>

<h2 id="partial-construction-is-the-dangerous-edge">Partial construction is the dangerous edge</h2>

<p>The expert lab is a partial-construction bug. A user is partly created before its confirmation token is fully initialized. A confirmation request using an empty-array token shape can land in that temporary state:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST /confirm?token[]=
</span></code></pre></div></div>

<p>This class appears whenever systems expose half-built rows, files, sessions, or jobs to other endpoints.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Hardening:</p>

<ul>
  <li>put check-and-act logic in transactions or atomic operations;</li>
  <li>use row locks, unique constraints, and compare-and-set for shared state;</li>
  <li>make coupons, balances, counters, and pending-email records single-owner transitions;</li>
  <li>bind async jobs to immutable snapshots;</li>
  <li>generate reset tokens with CSPRNGs, not timestamps;</li>
  <li>keep partial objects invisible until fully initialized;</li>
  <li>serialize critical actions per user or resource.</li>
</ul>

<p>Detection:</p>

<ul>
  <li>bursts of identical state-changing requests;</li>
  <li>impossible counts, such as multiple coupon successes;</li>
  <li>lockout counters lower than the observed attempts;</li>
  <li>order success after insufficient-funds responses;</li>
  <li>mismatched email recipient and confirmation body;</li>
  <li>duplicated reset tokens;</li>
  <li>empty-array or malformed confirmation tokens.</li>
</ul>

<p>Good race-condition defense is not “hope requests arrive slowly.” It is making the state transition indivisible.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="web-security" /><category term="methodology" /><category term="detection-engineering" /><category term="race-conditions" /><category term="business-logic" /><category term="turbo-intruder" /><category term="portswigger" /><summary type="html"><![CDATA[Race conditions are often described as “send requests at the same time.” That is the delivery technique, not the bug.]]></summary></entry><entry><title type="html">GraphQL Security Is Schema and Transport Control</title><link href="https://1392081456.github.io/2026/07/04/graphql-security-is-schema-and-transport-control/" rel="alternate" type="text/html" title="GraphQL Security Is Schema and Transport Control" /><published>2026-07-04T09:30:00+08:00</published><updated>2026-07-04T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/04/graphql-security-is-schema-and-transport-control</id><content type="html" xml:base="https://1392081456.github.io/2026/07/04/graphql-security-is-schema-and-transport-control/"><![CDATA[<p>GraphQL concentrates an application’s API surface behind one endpoint. That does not reduce the number of security boundaries. It hides them inside schema, resolver, and transport behavior.</p>

<p>The PortSwigger GraphQL labs are a compact reminder: the dangerous question is not only “where is the endpoint?” It is “what can this principal ask for, how many times, and through which browser mechanics?”</p>

<h2 id="schema-is-reconnaissance">Schema is reconnaissance</h2>

<p>Introspection turns the API into a map:</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="n">__schema</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">types</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="n">fields</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If private fields such as passwords, tokens, or hidden post metadata appear in the schema and resolvers do not enforce authorization, the client can simply ask for them.</p>

<p>Hiding the field in the UI is not a control. Resolver authorization is the control.</p>

<h2 id="hidden-endpoints-still-answer-universal-queries">Hidden endpoints still answer universal queries</h2>

<p>A useful endpoint probe is:</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">query</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">__typename</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">/api?query=query{__typename}</code> returns a type name, you found a GraphQL endpoint even if navigation never referenced it.</p>

<p>Blocking introspection with a string match is brittle. A filter looking for <code class="language-plaintext highlighter-rouge">__schema{</code> can miss valid GraphQL with whitespace:</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">query</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">__schema</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="n">queryType</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>GraphQL should be parsed and authorized, not filtered with raw-string assumptions.</p>

<h2 id="execution-semantics-affect-rate-limits">Execution semantics affect rate limits</h2>

<p>Aliases let one operation run many sibling fields or mutations:</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">mutation</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">a</span><span class="p">:</span><span class="w"> </span><span class="n">login</span><span class="p">(</span><span class="n">input</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="n">username</span><span class="p">:</span><span class="w"> </span><span class="s2">"carlos"</span><span class="p">,</span><span class="w"> </span><span class="n">password</span><span class="p">:</span><span class="w"> </span><span class="s2">"123456"</span><span class="p">})</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">success</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="n">b</span><span class="p">:</span><span class="w"> </span><span class="n">login</span><span class="p">(</span><span class="n">input</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="n">username</span><span class="p">:</span><span class="w"> </span><span class="s2">"carlos"</span><span class="p">,</span><span class="w"> </span><span class="n">password</span><span class="p">:</span><span class="w"> </span><span class="s2">"password"</span><span class="p">})</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">success</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>A limiter that counts one HTTP request misses the fact that many login attempts happened inside that request. API controls have to count semantic actions, not just network envelopes.</p>

<h2 id="transport-can-create-csrf">Transport can create CSRF</h2>

<p>GraphQL is often shown as JSON over POST. Some endpoints also accept form-urlencoded bodies:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query=...&amp;operationName=changeEmail&amp;variables=...
</code></pre></div></div>

<p>That can turn a mutation into a browser simple request, allowing a cross-site form POST with ambient cookies. State-changing GraphQL operations need CSRF protection and strict content-type policy.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Hardening:</p>

<ul>
  <li>disable or authenticate introspection;</li>
  <li>enforce authorization inside resolvers for fields and objects;</li>
  <li>treat ids as object references requiring ownership checks;</li>
  <li>reject regex-only introspection filters;</li>
  <li>restrict aliases and batching for sensitive mutations;</li>
  <li>rate-limit by action count, username, account, and IP;</li>
  <li>require JSON and CSRF tokens for state changes;</li>
  <li>reject GET for mutations.</li>
</ul>

<p>Detection:</p>

<ul>
  <li>introspection terms with unusual whitespace or encoding;</li>
  <li>many aliases calling the same mutation;</li>
  <li>GraphQL query strings on unexpected paths;</li>
  <li>low-privilege users requesting credential-like fields;</li>
  <li>form-urlencoded traffic to GraphQL endpoints;</li>
  <li>state-changing mutations without CSRF signals.</li>
</ul>

<p>GraphQL’s power is legitimate. The mistake is letting the schema, resolver layer, and browser transport rules drift out of the threat model.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="web-security" /><category term="methodology" /><category term="detection-engineering" /><category term="graphql" /><category term="api-security" /><category term="csrf" /><category term="portswigger" /><summary type="html"><![CDATA[GraphQL concentrates an application’s API surface behind one endpoint. That does not reduce the number of security boundaries. It hides them inside schema, resolver, and transport behavior.]]></summary></entry><entry><title type="html">Prototype Pollution Is Property Lookup Abuse</title><link href="https://1392081456.github.io/2026/07/03/prototype-pollution-is-property-lookup-abuse/" rel="alternate" type="text/html" title="Prototype Pollution Is Property Lookup Abuse" /><published>2026-07-03T09:30:00+08:00</published><updated>2026-07-03T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/03/prototype-pollution-is-property-lookup-abuse</id><content type="html" xml:base="https://1392081456.github.io/2026/07/03/prototype-pollution-is-property-lookup-abuse/"><![CDATA[<p>Prototype pollution is often described as a write bug: get <code class="language-plaintext highlighter-rouge">__proto__</code> into a parser, write to <code class="language-plaintext highlighter-rouge">Object.prototype</code>, win.</p>

<p>That is only the primitive. The impact is a read bug. Some later code looks for an own property, does not find one, and silently accepts the inherited value.</p>

<p>The PortSwigger prototype pollution labs are useful because they separate those two halves.</p>

<h2 id="the-chain-has-three-parts">The chain has three parts</h2>

<p>A real chain needs:</p>

<ul>
  <li>a source that writes to the prototype;</li>
  <li>a gadget that reads a missing property;</li>
  <li>a sink that treats the inherited value as code, configuration, or authority.</li>
</ul>

<p>Client-side examples include dynamic script URLs and <code class="language-plaintext highlighter-rouge">eval()</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>?__proto__[transport_url]=data:,alert(1);
</code></pre></div></div>

<p>Server-side examples include authorization flags and child process options:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"__proto__"</span><span class="p">:{</span><span class="nl">"isAdmin"</span><span class="p">:</span><span class="kc">true</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>

<p>The same pollution source can be harmless or critical depending on what property the application reads later.</p>

<h2 id="sanitizers-fail-when-they-are-not-parsers">Sanitizers fail when they are not parsers</h2>

<p>One lab uses a one-pass key sanitizer. Splitting the forbidden key is enough:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>?__pro__proto__to__[transport_url]=data:,alert(1);
</code></pre></div></div>

<p>Another blocks <code class="language-plaintext highlighter-rouge">__proto__</code> but misses the equivalent object path:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"constructor"</span><span class="p">:{</span><span class="nl">"prototype"</span><span class="p">:{</span><span class="nl">"isAdmin"</span><span class="p">:</span><span class="kc">true</span><span class="p">}}}</span><span class="w">
</span></code></pre></div></div>

<p>Prototype pollution filters need recursive structural validation. A blacklist applied once to a raw string is not enough.</p>

<h2 id="non-reflective-detection-matters">Non-reflective detection matters</h2>

<p>Server-side pollution may not reflect arbitrary properties. That does not mean it failed.</p>

<p>One safer probe is to pollute a status-code property, then trigger a controlled parse error:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"__proto__"</span><span class="p">:{</span><span class="nl">"status"</span><span class="p">:</span><span class="mi">555</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>

<p>If the error object changes status, the prototype was modified without needing destructive impact. Similar low-impact probes include JSON spacing and charset behavior.</p>

<h2 id="child-process-gadgets-are-configuration-bugs">Child process gadgets are configuration bugs</h2>

<p>The later labs show why inherited options are dangerous. If a maintenance job spawns a Node child process, inherited <code class="language-plaintext highlighter-rouge">execArgv</code> can add runtime flags. If code passes an options object to <code class="language-plaintext highlighter-rouge">execSync()</code>, inherited <code class="language-plaintext highlighter-rouge">shell</code> and <code class="language-plaintext highlighter-rouge">input</code> can control execution behavior.</p>

<p>The fix is not just filtering request keys. Sensitive option objects should be constructed explicitly and should not inherit attacker-controlled prototype state.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Hardening:</p>

<ul>
  <li>reject <code class="language-plaintext highlighter-rouge">__proto__</code>, <code class="language-plaintext highlighter-rouge">constructor</code>, and <code class="language-plaintext highlighter-rouge">prototype</code> recursively;</li>
  <li>use <code class="language-plaintext highlighter-rouge">Object.create(null)</code> for merge targets and option objects;</li>
  <li>check authorization with own properties only;</li>
  <li>avoid string-based JavaScript sinks;</li>
  <li>construct child process options from known fields only;</li>
  <li>test <code class="language-plaintext highlighter-rouge">--disable-proto=throw</code> in Node deployments;</li>
  <li>keep parser and merge libraries current.</li>
</ul>

<p>Detection:</p>

<ul>
  <li>prototype-key strings in query, fragment, JSON, or cookie data;</li>
  <li>split-key variants that reconstruct dangerous names;</li>
  <li>JSON spacing, error status, or charset changes after profile updates;</li>
  <li>ordinary users seeing admin-only links;</li>
  <li>maintenance jobs failing after account-data writes;</li>
  <li>unexpected <code class="language-plaintext highlighter-rouge">--eval</code>, shell, or input options in spawned processes.</li>
</ul>

<p>The core defensive question is simple: can this object read inherited authority or configuration? If yes, a parser bug can become a system bug.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="web-security" /><category term="methodology" /><category term="detection-engineering" /><category term="prototype-pollution" /><category term="nodejs" /><category term="dom-xss" /><category term="portswigger" /><summary type="html"><![CDATA[Prototype pollution is often described as a write bug: get __proto__ into a parser, write to Object.prototype, win.]]></summary></entry><entry><title type="html">Targeted Scanning Is a Manual Testing Tool</title><link href="https://1392081456.github.io/2026/07/02/targeted-scanning-is-a-manual-testing-tool/" rel="alternate" type="text/html" title="Targeted Scanning Is a Manual Testing Tool" /><published>2026-07-02T09:30:00+08:00</published><updated>2026-07-02T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/02/targeted-scanning-is-a-manual-testing-tool</id><content type="html" xml:base="https://1392081456.github.io/2026/07/02/targeted-scanning-is-a-manual-testing-tool/"><![CDATA[<p>The useful version of automated scanning is not “scan everything and wait.” It is “I think this boundary matters, so test this boundary hard.”</p>

<p>The PortSwigger Essential Skills labs make that distinction explicit. They are not really about new bug classes. They are about using Burp Scanner as a focused instrument during manual testing.</p>

<h2 id="the-human-chooses-the-boundary">The human chooses the boundary</h2>

<p>In the file-read lab, the time limit changes the workflow. A full-site scan may eventually find the issue, but it burns time on low-value requests.</p>

<p>The better sequence is simple:</p>

<ul>
  <li>inspect normal traffic;</li>
  <li>find the request most likely to read a server-side resource;</li>
  <li>run a targeted scan on that request;</li>
  <li>take the scanner’s vector back to Repeater;</li>
  <li>reduce it to a proof that reads <code class="language-plaintext highlighter-rouge">/etc/passwd</code>.</li>
</ul>

<p>The scanner accelerates the middle of the process. It does not decide what matters.</p>

<h2 id="hidden-inputs-are-still-inputs">Hidden inputs are still inputs</h2>

<p>The non-standard data-structure lab is the more important lesson. The authenticated cookie contains a visible structure:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>username:session-id
</code></pre></div></div>

<p>That is not one opaque value. It is two parsed fields sharing one cookie. Selecting only the username portion as an insertion point lets Scanner test the field the application actually trusts.</p>

<p>Once the stored XSS finding appears, the final proof preserves the session-id half and changes only the username half. Without that preservation, the request loses its authentication state.</p>

<h2 id="scanner-output-needs-interpretation">Scanner output needs interpretation</h2>

<p>Scanner findings are not the end of testing. They are leads:</p>

<ul>
  <li>What exact parser accepted the input?</li>
  <li>Which sub-field became trusted data?</li>
  <li>Which output context rendered it?</li>
  <li>What is the smallest proof?</li>
  <li>What server-side invariant failed?</li>
</ul>

<p>Those questions turn a scanner result into an engineering fix.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Hardening:</p>

<ul>
  <li>keep session cookies opaque and random;</li>
  <li>avoid packing user-controlled fields into authentication cookies;</li>
  <li>sign and schema-check structured values when structure is unavoidable;</li>
  <li>treat file access as resource lookup, not path concatenation;</li>
  <li>canonicalize paths before enforcing directory containment;</li>
  <li>encode stored content for the exact browser context.</li>
</ul>

<p>Detection:</p>

<ul>
  <li>traversal strings or absolute paths in resource parameters;</li>
  <li>HTML/event-handler payloads in cookie sub-fields;</li>
  <li>one insertion point receiving many scanner payload variants;</li>
  <li>privileged browser sessions causing unexpected outbound DNS or HTTP callbacks.</li>
</ul>

<p>Targeted scanning works because it keeps automation attached to a manual model of the application. The model is the work; the scanner is the amplifier.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="web-security" /><category term="methodology" /><category term="detection-engineering" /><category term="burp-suite" /><category term="scanner" /><category term="methodology" /><category term="portswigger" /><summary type="html"><![CDATA[The useful version of automated scanning is not “scan everything and wait.” It is “I think this boundary matters, so test this boundary hard.”]]></summary></entry><entry><title type="html">JWT Security Is Verification Policy</title><link href="https://1392081456.github.io/2026/07/01/jwt-security-is-verification-policy/" rel="alternate" type="text/html" title="JWT Security Is Verification Policy" /><published>2026-07-01T09:30:00+08:00</published><updated>2026-07-01T09:30:00+08:00</updated><id>https://1392081456.github.io/2026/07/01/jwt-security-is-verification-policy</id><content type="html" xml:base="https://1392081456.github.io/2026/07/01/jwt-security-is-verification-policy/"><![CDATA[<p>JWTs are easy to mistake for security because they look structured and cryptographic. The structure is not the security. The verification policy is.</p>

<p>The eight PortSwigger JWT labs move through the usual failure modes: no verification, <code class="language-plaintext highlighter-rouge">alg:none</code>, weak HMAC secrets, attacker-controlled keys, <code class="language-plaintext highlighter-rouge">kid</code> injection, and algorithm confusion.</p>

<p>This series was re-run and live-verified on 2026-05-30 as 8/8 solved. The
no-exposed-key lab is worth calling out: public key recovery from several
valid RS256 signatures was enough to trigger HS256 confusion without recovering
the private key.</p>

<h2 id="decode-is-not-verify">Decode is not verify</h2>

<p>The first failure is using a JWT library to parse claims without verifying the signature. If changing:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"sub"</span><span class="p">:</span><span class="s2">"administrator"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>works without re-signing, the server is authenticating decoded JSON, not a verified token.</p>

<p>The <code class="language-plaintext highlighter-rouge">alg:none</code> variant is only slightly different. The token declares:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"alg"</span><span class="p">:</span><span class="s2">"none"</span><span class="p">,</span><span class="nl">"typ"</span><span class="p">:</span><span class="s2">"JWT"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>and leaves the signature empty:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>header.payload.
</code></pre></div></div>

<p>No authentication system should accept this mode.</p>

<h2 id="hmac-secrets-are-credentials">HMAC secrets are credentials</h2>

<p>Weak HMAC secrets turn JWTs into offline password-cracking targets:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hashcat <span class="nt">-a</span> 0 <span class="nt">-m</span> 16500 &lt;jwt&gt; jwt.secrets.list
</code></pre></div></div>

<p>Once the secret is recovered, claims can be changed and re-signed. Treat HMAC JWT secrets like passwords with high entropy and rotation requirements.</p>

<h2 id="key-discovery-must-be-trusted">Key discovery must be trusted</h2>

<p>The key-header labs are all versions of the same bug: the attacker influences which key verifies their token.</p>

<p><code class="language-plaintext highlighter-rouge">jwk</code> embeds a public key directly in the header. <code class="language-plaintext highlighter-rouge">jku</code> points the server to an attacker-hosted JWKS. <code class="language-plaintext highlighter-rouge">kid</code> is supposed to select a known key, but becomes path traversal:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"kid"</span><span class="p">:</span><span class="s2">"../../../../../../../dev/null"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Key IDs should be opaque references into a trusted registry. They should not be URLs, files, or queries selected by the token.</p>

<h2 id="algorithm-confusion-is-type-confusion-for-keys">Algorithm confusion is type confusion for keys</h2>

<p>RS256 uses a private key to sign and a public key to verify. HS256 uses one shared secret for both.</p>

<p>Algorithm confusion appears when the verifier trusts the token’s <code class="language-plaintext highlighter-rouge">alg</code> and uses an RSA public key as an HMAC secret:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"alg"</span><span class="p">:</span><span class="s2">"HS256"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If the public key is exposed, it can be used to sign a forged HS256 token. If it is not exposed, it may still be derived from multiple valid RS256 tokens. The private key is not broken; the verifier’s key-type policy is.</p>

<h2 id="defender-notes">Defender notes</h2>

<p>Hardening:</p>

<ul>
  <li>pin accepted algorithms server-side;</li>
  <li>reject <code class="language-plaintext highlighter-rouge">none</code>;</li>
  <li>use verify APIs for authentication paths;</li>
  <li>use strong managed secrets for HMAC;</li>
  <li>treat <code class="language-plaintext highlighter-rouge">jwk</code>, <code class="language-plaintext highlighter-rouge">jku</code>, and <code class="language-plaintext highlighter-rouge">kid</code> as trusted-registry selectors only;</li>
  <li>allowlist JWKS origins;</li>
  <li>validate <code class="language-plaintext highlighter-rouge">kid</code> as an opaque ID;</li>
  <li>keep HS and RS verification code paths separate;</li>
  <li>avoid trusting high-risk authorization claims without server-side checks.</li>
</ul>

<p>Detection:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">alg:none</code>;</li>
  <li>unexpected <code class="language-plaintext highlighter-rouge">jwk</code> or external <code class="language-plaintext highlighter-rouge">jku</code>;</li>
  <li><code class="language-plaintext highlighter-rouge">kid</code> containing traversal or <code class="language-plaintext highlighter-rouge">/dev/null</code>;</li>
  <li>RS256 tokens suddenly becoming HS256;</li>
  <li>role or subject jumps without a fresh login;</li>
  <li>outbound JWKS fetches to unfamiliar hosts.</li>
</ul>

<p>JWT failures are usually policy failures. The token says what it wants to be; the server must decide what it is willing to verify.</p>]]></content><author><name>Colorful White</name><email>colorfulwhitez@gmail.com</email></author><category term="web-security" /><category term="methodology" /><category term="detection-engineering" /><category term="jwt" /><category term="jwk" /><category term="jku" /><category term="algorithm-confusion" /><category term="portswigger" /><summary type="html"><![CDATA[JWTs are easy to mistake for security because they look structured and cryptographic. The structure is not the security. The verification policy is.]]></summary></entry></feed>