Is Total VPN - один ВПН для всего safe?

High risk

Total VPN - один ВПН для всего is high risk. On popup open, the worker fetches config from GitHub Pages, Blogspot, or Google Docs, ROT-3 decoded. A 'u' value overwrites the payment API domain; 's' is a base64 proxy list. The operator can edit these pages anytime; confirmed live.

katia.noomovav2.0Chrome Web Store
72Risk
Who publishes it

katia.noomova - 7 other listings from the same operator, 2 of them carrying a finding

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

Publishers can request a review.

Findings

Payment domain and proxy list swapped via dead-drop config pages

HighunwantedCWE-829Dynamic sandbox

On popup open, the worker fetches config from GitHub Pages, Blogspot, or Google Docs, ROT-3 decoded.

What actually happens

You did this

You open the VPN extension's popup.

The extension did this

The extension fetches a configuration file from a public page it controls and uses it to set which domain receives your payment and account requests.

The same response also supplies the list of proxy servers your browsing traffic is routed through.

Captured request

GEThttps://katnmv.github.io/exs/list.json

Returned a ROT-3-obfuscated body (about 1.9 KB) observed during dynamic analysis on popup open. If this host fails, the extension retries https://twexst.blogspot.com/p/data2.html and then https://docs.google.com/document/d/1SFsBZ6Nsz1WH86YIKZyuT_KSzE_Ns36MS_78WOblpU0/edit.

Headers
Cache-Control
no-cache

Why you can't catch this in DevTools

The fetched body is encoded with a 3-position Caesar letter shift so it is not readable as JSON on the wire. Decoding it reveals the replacement payment domain ('u') and a base64-encoded server list ('s').

Decoded value
{  "u": "mainapi.store",  "s": [    "196.16.2.115:9037:RD4xCy:9XFUs|2026-07-14|gb|rmd"  ]}

The code that does this

Remote config replaces the payment API domain and extends the proxy server list

Readable version
popup/main.js
var c=showx(data.trim(),3);          // ROT-3 decodevar j=JSON.parse(c);if(typeof(j['u'])!='undefined'){  if(j['u'].length>0){    ucheckout="https://"+j['u'];        // replace payment/account API domain    chrome.storage.local.set({"apiurl":j['u']})  }}if(typeof(j['s'])!='undefined'){  chrome.storage.local.set({"servers":JSON.stringify(j['s'])});  j['s'].forEach(function(e,i){    let k=atob(e).split("|");           // host:port:user:pass | expiry | country | label    a1.push(k[0]);  });  serverConfigs.unshift(...a1);         // prepend remote servers}
js/checkout.js / popup/main.js
// The replaced domain becomes the base for every payment/account call:$.post(ucheckout+"/api/v"+acheckout+"/check_ssid",{id:id,...});$.post(ucheckout+"/api/v"+acheckout+"/getvpn",{id:id,...});$.post(ucheckout+"/api/v"+acheckout+"/setcode",{id:id,t:tcheckout,c:c});

Public pages used to deliver the configuration

    • katnmv.github.io

    Primary dead-drop: serves exs/list.json, the ROT-3-encoded config carrying the payment domain and server list. GitHub Pages, editable by whoever controls the repository.

    • twexst.blogspot.com

    First fallback dead-drop: the page /p/data2.html carries the same config. Blogspot, editable by the post owner.

    • docs.google.com

    Second fallback dead-drop: a Google Docs document (id 1SFsBZ6Nsz1WH86YIKZyuT_KSzE_Ns36MS_78WOblpU0) carries the same config. Editable by the document owner.

    • mainapi.store

    Default payment/account API domain shipped in conf.js; replaceable at runtime by the 'u' field from any of the dead-drop pages.

Reproduce it yourself

Fetches the GitHub Pages config, applies the ROT-3 decode the extension uses, parses the JSON, and prints the replacement payment domain plus each decoded proxy server entry.

Requires
  • Node.js 18+
decode-config.js · js
// Reproduces the extension's config decode. Node 18+ (global fetch).const URLS = [  'https://katnmv.github.io/exs/list.json',  'https://twexst.blogspot.com/p/data2.html',  'https://docs.google.com/document/d/1SFsBZ6Nsz1WH86YIKZyuT_KSzE_Ns36MS_78WOblpU0/edit'];function rot(text, shift) {            // matches showx(text,3)  return text.split('').map((c) => {    if (/[a-z]/i.test(c)) {      const code = c.charCodeAt(0);      const base = code >= 65 && code <= 90 ? 65 : 97;      return String.fromCharCode(((code - base - shift + 26) % 26) + base);    }    return c;  }).join('');}(async () => {  let body;  for (const u of URLS) {    try { const r = await fetch(u, { headers: { 'Cache-Control': 'no-cache' } });          if (r.ok) { body = await r.text(); break; } } catch {}  }  if (!body) { console.error('no config fetched'); return; }  const j = JSON.parse(rot(body.trim(), 3));  if (j.u) console.log('payment API domain ->', j.u);  (j.s || []).forEach((e) => {    const [hostPort, expiry, country, label] = Buffer.from(e, 'base64').toString().split('|');    console.log('proxy server ->', hostPort, '| expiry', expiry, '| country', country, '| label', label);  });})();
How to run it
  1. 1Save as decode-config.js.
  2. 2Run node decode-config.js.
  3. 3Read the printed payment API domain and the decoded proxy server entries.
Updated 22 October 2026nkhfdlilombfkfbnbdfephaaaifaollp