<?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: Nishant Singh</title>
    <description>The latest articles on DEV Community by Nishant Singh (@nishant_singh_777).</description>
    <link>https://dev.to/nishant_singh_777</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%2F4172023%2F355fc013-05de-41bb-99a2-540e25eae7fc.png</url>
      <title>DEV Community: Nishant Singh</title>
      <link>https://dev.to/nishant_singh_777</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nishant_singh_777"/>
    <language>en</language>
    <item>
      <title>3 Real Bugs I Found: From CTF Flags to a Live Smart Contract Vulnerability</title>
      <dc:creator>Nishant Singh</dc:creator>
      <pubDate>Thu, 08 Oct 2026 19:36:04 +0000</pubDate>
      <link>https://dev.to/nishant_singh_777/3-real-bugs-i-found-from-ctf-flags-to-a-live-smart-contract-vulnerability-3nj4</link>
      <guid>https://dev.to/nishant_singh_777/3-real-bugs-i-found-from-ctf-flags-to-a-live-smart-contract-vulnerability-3nj4</guid>
      <description>&lt;h1&gt;
  
  
  3 Real Bugs I Found: From CTF Flags to a Live Smart Contract Vulnerability
&lt;/h1&gt;

&lt;p&gt;I'm a student developer working through a 150-day self-directed ethical hacking roadmap, split between web security fundamentals and smart contract auditing (Soroban/Stellar). This post covers three bugs I found along the way — two from CTF challenges, and one from auditing my own smart contract project. Each one taught me something that applies far beyond the specific bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #1: Server-Side Template Injection → Full RCE
&lt;/h2&gt;

&lt;p&gt;This one came from a CTF web exploitation challenge called "SSTI1." The target was a Flask app using Jinja2 templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Confirm the injection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first move with any suspected SSTI is the classic math probe. If the app reflects user input back into a template, &lt;code&gt;{{7*7}}&lt;/code&gt; will render as &lt;code&gt;49&lt;/code&gt; instead of literally printing &lt;code&gt;{{7*7}}&lt;/code&gt;. That confirmed Jinja2 was evaluating my input as a template expression, not just displaying it as text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Escalate to RCE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you know Jinja2 is evaluating expressions, the next question is: what can you reach from there? Jinja2 templates run inside Python, and Python objects carry references to other objects through dunder attributes like &lt;code&gt;__class__&lt;/code&gt;, &lt;code&gt;__globals__&lt;/code&gt;, and &lt;code&gt;__builtins__&lt;/code&gt;. Jinja2 sandboxes templates from direct Python execution, but it doesn't block attribute access — which is the whole vulnerability.&lt;/p&gt;

&lt;p&gt;The payload that got me a shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;self.__init__.__globals__.__builtins__.__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'os'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;popen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;self&lt;/code&gt; — refers to the current template context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__init__.__globals__&lt;/code&gt; — walks back to the global namespace of the module, which includes &lt;code&gt;__builtins__&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__builtins__.__import__('os')&lt;/code&gt; — imports the &lt;code&gt;os&lt;/code&gt; module, bypassing the fact that &lt;code&gt;os&lt;/code&gt; was never directly exposed to the template&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.popen(cmd).read()&lt;/code&gt; — runs an arbitrary shell command and returns the output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there I had command execution, and the flag was sitting in a file I could just &lt;code&gt;cat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; SSTI is often dismissed as "just XSS with extra steps," but it's not — SSTI runs on the server, in the application's own process, with the application's own permissions. A sandboxed templating engine is only as safe as its weakest attribute path, and &lt;code&gt;__globals__&lt;/code&gt; is a well-known one in Jinja2, Mako, Twig, and others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #2: A Forgotten API Endpoint Leaked a Secret via Heap Dump
&lt;/h2&gt;

&lt;p&gt;This one came from a different CTF challenge, "head-dump." No obvious vulnerability was visible on the main site — no injection points, no broken auth. The clue was hidden in plain sight: the site's own blog post mentioned API documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Find the undocumented endpoint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Following that hint led to &lt;code&gt;/api-docs&lt;/code&gt;, a Swagger UI page listing the app's REST API. Most of it was expected stuff — login, user data, standard CRUD routes. But tucked under a "Diagnosing" section was something that should never be exposed publicly: &lt;code&gt;GET /heapdump&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Understand what a heap dump actually is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A heap dump is a snapshot of a running Node.js process's memory — every variable, every object, every string currently held in memory at that moment. It's meant for developers debugging memory leaks locally, never for a public-facing endpoint. If the app ever held a secret in memory (an API key, a session token, a password during a login check), it's sitting in that snapshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Extract the secret&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I downloaded the &lt;code&gt;.heapsnapshot&lt;/code&gt; file — about 11MB of minified JSON. The naive approach (grepping the file, or loading it into Chrome DevTools' Memory tab) didn't work; the file was corrupted on the first download and needed a re-download from a fresh instance.&lt;/p&gt;

&lt;p&gt;Once I had a clean file, standard text search tools choked on a single-line minified JSON blob that large. What worked was pulling the exact byte range around the flag marker directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Raw&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"academy{"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That found the flag string sitting in memory, exactly where it shouldn't have been.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Debug and diagnostic endpoints are an entire class of vulnerability that doesn't show up in a typical checklist of "common web vulns." They don't look dangerous because they're not designed to take input or return user data directly — but anything that exposes raw process memory is a direct line to whatever secrets the app is handling at runtime. The fix is simple: diagnostic routes like &lt;code&gt;/heapdump&lt;/code&gt;, &lt;code&gt;/debug&lt;/code&gt;, &lt;code&gt;/metrics&lt;/code&gt; should never ship to production without authentication, if they ship at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #3: A Live Authorization Bug in My Own Smart Contract
&lt;/h2&gt;

&lt;p&gt;The first two bugs were CTF challenges — built to be found. This one was different: a real authorization vulnerability I found while auditing my own project, &lt;a href="https://github.com/Nishantk16/chitchain" rel="noopener noreferrer"&gt;ChitChain&lt;/a&gt;, a Soroban (Stellar smart contract) implementation of a ROSCA/chit fund protocol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ChitChain has a function, &lt;code&gt;update_circle_status()&lt;/code&gt;, that's supposed to let the right party transition a chit circle between states (e.g., active → completed). Like most Soroban contracts, authorization is enforced with &lt;code&gt;require_auth()&lt;/code&gt; — a call that checks whether a specific address has cryptographically signed off on the transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The function was calling &lt;code&gt;require_auth()&lt;/code&gt; on the wrong party. It checked that &lt;code&gt;entry.admin&lt;/code&gt; had authorized the call, when the actual authorization should have been tied to the circle itself (&lt;code&gt;circle.require_auth()&lt;/code&gt;). In practice, this meant the permission check wasn't actually gating the action the way it was supposed to — the state transition could fail in ways that broke the circle's logic, because the party the contract trusted to authorize the change wasn't the party that should have had that authority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the tests didn't catch it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that matters most. The existing test suite used &lt;code&gt;mock_all_auths()&lt;/code&gt;, a Soroban testing helper that auto-approves every &lt;code&gt;require_auth()&lt;/code&gt; call in a test environment. It's genuinely useful for testing business logic without having to construct real signed authorizations for every single call — but it has a sharp edge: it makes authorization bugs invisible. If every &lt;code&gt;require_auth()&lt;/code&gt; call silently succeeds in tests, a test checking the wrong party will still pass, because mock_all_auths() doesn't care who was supposed to authorize — it approves everyone.&lt;/p&gt;

&lt;p&gt;I only found the actual bug by tracing the authorization logic by hand against the contract's intended access model, not by running the test suite. The tests were all green the entire time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.admin&lt;/span&gt;&lt;span class="nf"&gt;.require_auth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="n"&gt;circle&lt;/span&gt;&lt;span class="nf"&gt;.require_auth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A one-line fix, but it closes a real gap between "who the contract thinks is allowed to act" and "who the tests think is allowed to act."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; &lt;code&gt;mock_all_auths()&lt;/code&gt; (and equivalents in other frameworks — any blanket "auto-approve all auth checks" test helper) is a convenience that trades test-writing speed for a blind spot. A green test suite tells you your &lt;em&gt;business logic&lt;/em&gt; works under the assumption that authorization is correct — it says nothing about whether the authorization itself is correct. If you're auditing a Soroban contract (or any contract using similar test mocking patterns), grep for every &lt;code&gt;require_auth()&lt;/code&gt; call and manually verify &lt;em&gt;which&lt;/em&gt; party it's checking against the intended access-control model, independent of what the tests assert.&lt;/p&gt;

&lt;h2&gt;
  
  
  What These Three Bugs Have in Common
&lt;/h2&gt;

&lt;p&gt;On the surface these are three unrelated bugs — a templating engine, a debug endpoint, and a smart contract authorization check. But they share the same underlying lesson: &lt;strong&gt;the vulnerability lives in the gap between what a system is supposed to trust and what it actually trusts.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSTI trusts that sandboxing blocks dangerous code paths — it doesn't block attribute access, and that's enough.&lt;/li&gt;
&lt;li&gt;The heap dump endpoint trusts that "internal" diagnostic routes won't be found — they were documented in public API docs.&lt;/li&gt;
&lt;li&gt;The ChitChain bug trusts that a passing test suite means authorization is correct — &lt;code&gt;mock_all_auths()&lt;/code&gt; approves everyone, so it can't catch &lt;em&gt;who&lt;/em&gt; should've been checked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these required advanced exploitation. Each one required reading past the surface: tracing &lt;code&gt;__globals__&lt;/code&gt; instead of assuming the sandbox held, reading the API docs instead of assuming debug routes are hidden, and tracing &lt;code&gt;require_auth()&lt;/code&gt; by hand instead of trusting green tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Fits In My Roadmap
&lt;/h2&gt;

&lt;p&gt;This post covers Day 101-102 (CTF practice) and Day 81 (Soroban security auditing) of my 150-day ethical hacking roadmap, which I'm documenting day by day on GitHub (&lt;a href="https://github.com/Nishantk16/cypher-security" rel="noopener noreferrer"&gt;cypher-security&lt;/a&gt;). I'm currently past the 100-day mark, working through smart contract auditing, bug bounty hunting, and building a portfolio of real audit findings — including a full writeup of the ChitChain bug in my &lt;a href="https://github.com/Nishantk16/security-portfolio" rel="noopener noreferrer"&gt;security-portfolio&lt;/a&gt; repo.&lt;/p&gt;

&lt;p&gt;If you're working through something similar — CTFs, smart contract security, or just starting out in AppSec — I'd love to hear what you're working on. Feel free to connect or follow along with the roadmap.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
