<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: duchu.nft</title>
    <description>The latest articles on DEV Community by duchu.nft (@duchuaz).</description>
    <link>https://dev.to/duchuaz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4148260%2F944d32b4-b432-4613-977a-dcace7a1542c.jpg</url>
      <title>DEV Community: duchu.nft</title>
      <link>https://dev.to/duchuaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/duchuaz"/>
    <language>en</language>
    <item>
      <title>How a Fox and a Single Quote Broke the Critter Gallery — SQLi in Intigriti Challenge 0926</title>
      <dc:creator>duchu.nft</dc:creator>
      <pubDate>Tue, 29 Sep 2026 01:07:17 +0000</pubDate>
      <link>https://dev.to/duchuaz/how-a-fox-and-a-single-quote-broke-the-critter-gallery-sqli-in-intigriti-challenge-0926-4fhm</link>
      <guid>https://dev.to/duchuaz/how-a-fox-and-a-single-quote-broke-the-critter-gallery-sqli-in-intigriti-challenge-0926-4fhm</guid>
      <description>&lt;h1&gt;
  
  
  How a Fox and a Single Quote Broke the Critter Gallery — SQLi in Intigriti Challenge 0926
&lt;/h1&gt;

&lt;p&gt;My write-up for &lt;a href="https://go.intigriti.com/submit-solution" rel="noopener noreferrer"&gt;Intigriti Challenge 0926&lt;/a&gt;: a cute animal gallery hiding a textbook SQL injection behind a base64 &lt;code&gt;?pic=&lt;/code&gt; parameter. Solved 27/09/2026, submission &lt;code&gt;INTIGRITI-TAPV7AA2&lt;/code&gt; accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge
&lt;/h2&gt;

&lt;p&gt;The Critter Gallery shows one animal per page. The animal is picked via a base64-encoded &lt;code&gt;?pic=&lt;/code&gt; query parameter that decodes to the critter's name, e.g. &lt;code&gt;?pic=Rk9Y&lt;/code&gt; → &lt;code&gt;FOX&lt;/code&gt;. Beneath each picture sits a short description pulled from a database. Somewhere on the server, a second table — &lt;code&gt;secret_vault&lt;/code&gt; — holds the flag.&lt;/p&gt;

&lt;p&gt;The official tip: &lt;em&gt;"the gallery speaks different languages"&lt;/em&gt; — the same input is processed differently by two layers of the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  First look: two code paths, one input
&lt;/h2&gt;

&lt;p&gt;Requesting &lt;code&gt;FOX&lt;/code&gt; vs &lt;code&gt;Fox&lt;/code&gt; was the first tell. Both returned the fox description, but the ASCII art differed — one fell back to a default drawing. So the image lookup and the description lookup clearly don't share the same comparison logic: the art is matched one way (PHP, exact/case-sensitive), the description another (MySQL, case-insensitive collation with fullwidth folding). Two languages, exactly as the tip promised.&lt;/p&gt;

&lt;p&gt;That mismatch told me the description path talks directly to SQL — worth poking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the injection: the blank page
&lt;/h2&gt;

&lt;p&gt;Classic quote test, base64-wrapped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;'&lt;/code&gt; → &lt;strong&gt;blank white page&lt;/strong&gt; (zero-length response)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\&lt;/code&gt; → blank page too&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"&lt;/code&gt; → normal page, unaffected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single quote killing the page while a double quote does nothing means the input lands inside a &lt;strong&gt;single-quoted SQL string&lt;/strong&gt;. The backslash breaking it too confirms the quote isn't being escaped — it's raw string concatenation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming output: dumping all descriptions
&lt;/h2&gt;

&lt;p&gt;Next, a boolean break-out to prove the query result reaches the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?pic=JyBPUiAnMSc9JzE=        # base64(' OR '1'='1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The description box printed &lt;strong&gt;all 8 rows&lt;/strong&gt; instead of one. Injection confirmed, and the output lands in &lt;code&gt;div.desc&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Column count: UNION needs exactly one
&lt;/h2&gt;

&lt;p&gt;UNION-based extraction needs the right column count. Probing &lt;code&gt;ORDER BY&lt;/code&gt; / UNION with 1..n columns showed the query selects a &lt;strong&gt;single column&lt;/strong&gt; (&lt;code&gt;SELECT desc ...&lt;/code&gt;), and UNION output renders in the same description div.&lt;/p&gt;

&lt;p&gt;Fingerprinting via UNION:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?pic=JyBVTklPTiBTRUxFQ1QgQEB2ZXJzaW9uIC0tIC0=   # base64(' UNION SELECT @@version -- -)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;strong&gt;MySQL 8.0.46&lt;/strong&gt;, database &lt;code&gt;critter_gallery&lt;/code&gt;, tables &lt;code&gt;animals&lt;/code&gt; and &lt;code&gt;secret_vault&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vault
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;secret_vault&lt;/code&gt; has two columns: &lt;code&gt;id, note&lt;/code&gt;. One shot to dump it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;zzz&lt;/span&gt;&lt;span class="s1"&gt;' UNION SELECT GROUP_CONCAT(id,'&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="s1"&gt;',note SEPARATOR '&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="s1"&gt;') FROM secret_vault -- -
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="n"&gt;pic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;enp6JyBVTklPTiBTRUxFQ1QgR1JPVVBfQ09OQ0FUKGlkLCd&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;Jyxub3RlIFNFUEFSQVRPUiAnfCcpIEZST00gc2VjcmV0X3ZhdWx0IC0tIC0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The description box returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1~INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(flag as rendered; submitted value &lt;code&gt;INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2}&lt;/code&gt;, accepted ✅)&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Behavioral diffs are the real tip.&lt;/strong&gt; The &lt;code&gt;FOX&lt;/code&gt;/&lt;code&gt;Fox&lt;/code&gt; art mismatch revealed two comparison semantics before I sent a single quote. When one input gets two treatments, one of them is usually injectable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A blank page is an answer.&lt;/strong&gt; Zero-length responses on &lt;code&gt;'&lt;/code&gt; and &lt;code&gt;\&lt;/code&gt; (but not &lt;code&gt;"&lt;/code&gt;) fingerprint a single-quoted context with no escaping — no error message needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dump small, then go wide.&lt;/strong&gt; &lt;code&gt;' OR '1'='1&lt;/code&gt; (8 rows in the desc box) proved output control before I spent requests on schema enumeration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know your collations.&lt;/strong&gt; PHP exact-match vs MySQL case-insensitive/fullwidth-folding comparison is a classic desync primitive — it shows up in auth bypasses too, not just galleries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks to the Intigriti team for a fun challenge. Full probe trail (14 rounds, from recon to flag) is documented in my notes.&lt;/p&gt;

</description>
      <category>security</category>
      <category>sql</category>
      <category>bugbounty</category>
    </item>
  </channel>
</rss>
