Is РуТрекер VPN - расширение для доступа к сайту safe?

High risk

The extension fetches proxy server configurations at runtime from remote dead-drop URLs, allowing operators to silently reroute all proxied browser traffic.

RuTracker VPN retrieves its proxy server list from external sources including GitHub Pages, Blogspot, Google Docs, and Telegram using ROT3-obfuscated JSON, replacing the built-in server list stored in browser storage. This design gives the extension operator the ability to redirect all proxied traffic through any server without user action or an extension update. The extension also ships multiple shared proxy credentials in plaintext within its bundle and sends a persistent checkout identifier to api.hhos.ru on every subscription check.

sherechevskiyv2.4Chrome Web Store
75Risk
Who publishes it

sherechevskiy - 17 other listings from the same operator, 10 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
sherechevskiy

Shared hosts - 3 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

api.hhos.ru
Also called by 2 other listings: ВПН для ЧатГПТ / ChatGPT, ВПН для Gemini / VPN for Gemini
dtxtension.blogspot.com
Also called by 2 other listings: ВПН для ЧатГПТ / ChatGPT, ВПН для Gemini / VPN for Gemini
s-extension.github.io
Also called by 2 other listings: ВПН для ЧатГПТ / ChatGPT, ВПН для Gemini / VPN for Gemini

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Proxy and payment-API endpoints set at runtime from rotating dead-drop URLs

HighunwantedCWE-829Dynamic sandbox

This VPN extension's service worker fetches remote config from GitHub Pages, Blogspot, or a Google Doc.

What actually happens

You did this

You open the extension popup to use the proxy.

The popup sends a message to the service worker requesting the current configuration.

The extension did this

The service worker fetches a configuration file from a remote dead-drop URL and the popup applies it to local storage.

The decoded response replaces the proxy server list and the payment-API host stored on the device.

The code that does this

The dead-drop URLs and the fetch-with-fallback routine

Readable version

Decode, parse, and apply the remote config (popup/main.js)

popup/main.js
function getDcnfJson(data){  var c=showx(data.trim(),3);          // ROT3 decode  var j=JSON.parse(c);  if(j['u'] && j['u'].length>0){    ucheckout="https://"+j['u'];    chrome.storage.local.set({"apiurl":j['u']});   // override payment host  }  if(j['s']!==undefined){    chrome.storage.local.set({"servers":JSON.stringify(j['s'])});  // replace proxy list  }}function showx(text,shift){            // Caesar/ROT shift  return text.split('').map((ch)=>{    if(ch.match(/[a-z]/i)){      const code=ch.charCodeAt(0);      const offset=code>=65&&code<=90?65:97;      return String.fromCharCode(((code-offset-shift+26)%26)+offset);    }    return ch;  }).join('');}

Why you can't catch this in DevTools

The configuration file is served as JSON with a uniform ROT3 letter shift applied to the whole document, so the keys and values are not human-readable on the wire. Applying the inverse shift recovers the JSON, which carries the payment-API host (key 'u') and a list of base64-encoded proxy-server records (key 's'). Values shown are from the response observed during dynamic analysis (the proxy record is illustrative of the decoded host:port:login:pass|expiry|country format).

Decoded value
{"u":"obratx.shop","s":["MTY4LjgxLjc1LjEyNjo5MDkxOnRGQzQ3TjpCRHBtSnB8MjAyNi0wNy0xNHxpbg=="]}// 's' entry base64-decodes to://   168.81.75.126:9091:tFC47N:BDpmJp|2026-07-14|in//   host : port : login : pass | expiry | country//// 'u' = obratx.shop  -> chrome.storage.local['apiurl'] (overrides default api.hhos.ru)

Captured request

GEThttps://s-extension.github.io/DataExst/data.json

HTTP 200, ~4.6 KB ROT3-obfuscated JSON. Decoded key 'u' resolved to obratx.shop (replacing the default api.hhos.ru payment host); key 's' carried 65 base64-encoded proxy-server records that were written to chrome.storage.local['servers']. Observed during dynamic analysis on version 2.3.

Headers
Cache-Control
no-cache

Locations the configuration was fetched from, in fallback order

    • s-extension.github.io

    Primary config source (GitHub Pages, path /DataExst/data.json). Served the proxy list and payment host during analysis.

    • dtxtension.blogspot.com

    Fallback config source (Blogspot page /p/kronshtat.html) used if the primary host fails.

    • docs.google.com

    Fallback config source (a Google Docs document) used if the first two fail.

Reproduce it yourself

Fetches the extension's configuration dead-drop URL, reverses the ROT3 letter shift, and prints the decoded JSON along with each base64-decoded proxy-server record so you can see the payment host and server list the extension would apply.

Requires
  • Node.js 18+
decode-rutracker-config.js · js
// node decode-rutracker-config.js// Reverses the ROT3 obfuscation the extension applies to its remote config.const URLS = [  'https://s-extension.github.io/DataExst/data.json',  'https://dtxtension.blogspot.com/p/kronshtat.html',];function rot(text, shift) {  return text.split('').map((ch) => {    if (ch.match(/[a-z]/i)) {      const code = ch.charCodeAt(0);      const offset = code >= 65 && code <= 90 ? 65 : 97;      return String.fromCharCode(((code - offset - shift + 26) % 26) + offset);    }    return ch;  }).join('');}(async () => {  for (const url of URLS) {    try {      const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } });      if (!res.ok) continue;      const wire = (await res.text()).trim();      const decoded = rot(wire, 3);          // showx(data, 3)      const j = JSON.parse(decoded);      console.log('source        :', url);      console.log('payment host  :', j.u || j.url || '(none)');      const servers = j.s || j.servers || [];      console.log('server records:', servers.length);      servers.forEach((b64, i) => {        const rec = Buffer.from(b64, 'base64').toString('utf8');        console.log(`  [${i}] ${rec}`);  // host:port:login:pass|expiry|country      });      return;    } catch (e) {      console.warn('skip', url, e.message);    }  }  console.error('no config source responded');})();
How to run it
  1. 1Save this file as decode-rutracker-config.js.
  2. 2Run node decode-rutracker-config.js (Node 18+ has a built-in fetch).
  3. 3Read the printed payment host and the base64-decoded proxy-server records (host:port:login:pass|expiry|country).

Shared proxy usernames and passwords shipped in plaintext in the bundle

MediumunwantedCWE-522Dynamic sandbox

This proxy/VPN extension ships its server list as plain text in js/conf.js.

What actually happens

You did this

You enable a proxy server from the extension popup.

The selected server entry is parsed into host, port, login, and password.

The extension did this

When the proxy asks for authentication, the extension replies with the username and password embedded in its bundle.

The onAuthRequired handler returns the login/password parsed from the host:port:login:pass entry in js/conf.js.

Distinct plaintext proxy credential pairs found in js/conf.js (passwords redacted)

  • Shared port-20000 login
    DHaNuRsEhGAL007 : <redacted>

    One username/password pair reused across 20-plus proxy server IP entries on port 20000.

  • Discord-named proxy login
    discord_0.0.2 : <redacted>

    Credential for srv10.undiscord.com on port 443.

  • freeruproxy.ink login
    openproxy : <redacted>

    Credential reused for nl-hub and us-hub freeruproxy.ink entries on port 443.

  • accessyoutube login
    ytpass_50JtWMp4 : <redacted>

    Credential for srv11.accessyoutube.com on port 443.

The code that does this

Credentials in conf.js and the handler that supplies them

Readable version

Parsing the tuple and answering proxy auth (js/bg.js)

js/bg.js
// getProxyData(): split the selected entry into its partsconst [host,port,login,pass]=dpx.split(":");let ob={host,port,login,pass};proxyData=ob;// authHandler: respond to the server's auth prompt with those valuesauthHandler=function(details){  if(proxyData && proxyData.login && proxyData.pass){    return { authCredentials:{ username:proxyData.login, password:proxyData.pass } };  }  return { cancel:true };};chrome.webRequest.onAuthRequired.addListener(authHandler,{urls:["<all_urls>"]},["blocking"]);

Check if you're affected

Reads js/conf.js from the unpacked extension, parses the serverConfigs array, and lists the distinct login:password pairs and how many server entries reuse each one. Run it against the extension package to confirm the credentials are present in plaintext.

Requires
  • Node.js 18+
extract-proxy-creds.js · js
// node extract-proxy-creds.js /path/to/extracted/js/conf.jsconst fs = require('fs');const path = process.argv[2] || 'js/conf.js';const src = fs.readFileSync(path, 'utf8');// Pull the serverConfigs array literalconst m = src.match(/serverConfigs\s*=\s*\[([\s\S]*?)\]/);if (!m) { console.error('serverConfigs array not found'); process.exit(1); }const entries = [...m[1].matchAll(/['\"]([^'\"]+)['\"]/g)].map((x) => x[1]);const byPair = new Map();for (const e of entries) {  const parts = e.split(':');  if (parts.length >= 4) {                 // host:port:login:pass    const login = parts[2];    const pass = parts[3];    const key = `${login}:${pass}`;    byPair.set(key, (byPair.get(key) || 0) + 1);  }}console.log(`entries: ${entries.length}, distinct credential pairs: ${byPair.size}`);for (const [pair, count] of byPair) {  const [login] = pair.split(':');  console.log(`  login=${login}  reused across ${count} server entr${count === 1 ? 'y' : 'ies'}`);}
How to run it
  1. 1Unpack the .crx (it's a zip).
  2. 2Save as extract-proxy-creds.js.
  3. 3Run node extract-proxy-creds.js path/to/extracted/js/conf.js.
  4. 4Review the credential-pair count and reuse per login.

Where it sends data

Destinations our analysis observed РуТрекер VPN - расширение для доступа к сайту contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • docs.google.com

    РуТрекер VPN - расширение для доступа к сайту sends data to docs.google.com. 23 other extensions we have analyzed send data here.

  • s-extension.github.io

    РуТрекер VPN - расширение для доступа к сайту sends data to s-extension.github.io. 9 other extensions we have analyzed send data here.

  • dtxtension.blogspot.com

    РуТрекер VPN - расширение для доступа к сайту sends data to dtxtension.blogspot.com. 7 other extensions we have analyzed send data here.

  • api.hhos.ru

    РуТрекер VPN - расширение для доступа к сайту sends data to api.hhos.ru. One other extension we have analyzed sends data here.

Updated 22 October 2026llngkcnndicadfcbikbjnhbcikpmaknj