Sockets: Inter-Process Communication Endpoints, Local and Remote

Sockets: Inter-Process Communication Endpoints, Local and Remote

A socket serves as a fundamental endpoint that facilitates communication between processes. We encountered an example of its remote variant in the simple, yet vulnerable, TCP server. Sockets are among the most common Inter-Process Communication (IPC) channels and, consequently, represent a rich and diverse source of potential attack vectors.

Unix Domain Sockets (UDSs): Local Filesystem Exposure

Unix-like operating systems (such as Linux, macOS, and BSD variants) additionally support Unix Domain Sockets (UDSs). UDSs are a local variant of sockets that operate in different modes—specifically stream, datagram, and sequenced packet modes—mirroring the functionalities of TCP, UDP, and SCTP, respectively. However, a significant advantage of UDSs is that they do not incur the overhead of a full network protocol layer, resulting in faster communication.

In adherence to the venerable “everything is a file” philosophy prevalent in Unix-like systems, you can represent UDSs as actual files within the operating system’s filesystem. This stands in contrast to network sockets, which are addressed using an IP address and a port number. While convenient for developers, binding a UDS to a filesystem pathname inherently exposes it to the numerous namespace hijacking issues that plague file-based IPC. Moreover, by delegating access control for UDSs to the underlying filesystem permissions, they also open up the possibility of inappropriate file permissions being set, which can lead to unauthorized access.

A notable example of a vulnerability arising from this characteristic is CVE-2022-21950, discovered in Canna, a Japanese Kana–Kanji server. The vulnerability stemmed from the hardcoded directory /tmp/.iroha_unix, which contained the UDS utilized by Canna. As meticulously detailed in the bug report (Bugzilla: 1199280 – Canna: World writable /tmp/.iroha_unix directory allows privilege escalation), the openSUSE operating system had previously patched an earlier bug in Canna. This patch involved modifying the Canna systemd service configuration to remove the /tmp/.iroha_unix directory both before and after the cannaserver executed, using the ExecPre and ExecStopPost directives:

Bash
ExecPre=/bin/rm -rf /tmp/.iroha_unix
ExecStart=/usr/sbin/cannaserver -s -u wnn -r /var/lib/canna
ExecStopPost=/bin/rm -rf /tmp/.iroha_unix

ExecPre=/bin/rm -rf /tmp/.iroha_unix

Effect: Before starting cannaserver, this removes the stale /tmp/.iroha_unix directory or socket to avoid conflicts.


ExecStart=/usr/sbin/cannaserver -s -u wnn -r /var/lib/canna

Effect: This launches the Canna server with specific user and resource settings.


ExecStopPost=/bin/rm -rf /tmp/.iroha_unix

Effect: Ensures cleanup after shutdown to prevent leftover IPC sockets or files from interfering with future runs.

Unfortunately, this fix inadvertently introduced a new window of opportunity for a malicious user. Because the ExecPre command removed the directory, there was a brief period when the directory did not exist. During this window, another low-privileged user could create the /tmp/.iroha_unix directory with world-writable permissions. Previously, this directory was configured in systemd to be created by the root user at startup, leaving no opportunity for a low-privileged attacker to pre-create or override its permissions. If Canna subsequently created its UDS within this attacker-controlled, world-writable directory, an attacker could then replace the legitimate UDS with their own controlled socket. This effectively created a man-in-the-middle (MITM) attack scenario, allowing the attacker to intercept and potentially manipulate sensitive Japanese language user input within the operating system.

Mitigating UDS Attacks: Ancillary Data

UDSs offer a powerful built-in mechanism to prevent such namespace hijacking and unauthorized access, as described in the unix(7) Linux manual page: “UNIX domain sockets support passing file descriptors or process credentials to other processes using ancillary data.” This advanced feature enables sockets to reliably identify the sending process when a message is received by accepting additional data in the struct ucred format:

C
struct ucred {
    pid_t pid;  /* Process ID of the sending process */
    uid_t uid;  /* User ID of the sending process */
    gid_t gid;  /* Group ID of the sending process */
};

For instance, a privileged program that is listening on a UDS can use this feature to ensure that all messages it receives truly originate from processes running under specific privileged user groups, thereby providing an invaluable additional layer of access control. Since this credential passing mechanism operates directly within the kernel, it is generally impossible to spoof credentials in a typical exploitation scenario, making it a robust security feature.

It’s also worth noting that Windows began supporting UDSs in 2017 (AF_UNIX comes to Windows!). As operating systems continue to add and update various forms of IPC, the potential attack surface of software continuously expands, requiring researchers to stay abreast of these evolving capabilities.

Named Pipes: Windows’ IPC Paradigm

Named pipes represent another critical mechanism through which processes can communicate using a paradigm that closely resembles file operations. However, on Windows systems, named pipes possess a distinct characteristic: they maintain their own access control model, which is separate from the default filesystem’s access control. This separation, while providing flexibility, also introduces an additional layer of potential authorization issues if not configured correctly.

Windows Named Pipe Filesystem: Multi-Client Communication

Unlike named pipes on Unix-like systems, which traditionally allow access by only one reader process and one writer process at a time, Windows named pipes are designed to facilitate communication between a single server and multiple clients within their own specialized named pipe filesystem. Due to a unique namespace property of Windows named pipes, different processes can even create multiple server instances of a named pipe with the same name concurrently.

Let’s examine the CreateNamedPipe function, a core Windows API call responsible for creating an instance of a named pipe:

C
HANDLE CreateNamedPipeA(
  LPCSTR lpName,                  // Name of the named pipe (e.g., "\\\\.\\pipe\\MyPipe")
  DWORD  dwOpenMode,             // Pipe access mode (e.g., PIPE_ACCESS_DUPLEX for read/write)
  DWORD  dwPipeMode,             // Pipe behavior (e.g., PIPE_TYPE_MESSAGE, PIPE_WAIT)
  DWORD  nMaxInstances,          // Max number of instances (PIPE_UNLIMITED_INSTANCES or a fixed number)
  DWORD  nOutBufferSize,         // Size of the output buffer (server to client), in bytes
  DWORD  nInBufferSize,          // Size of the input buffer (client to server), in bytes
  DWORD  nDefaultTimeOut,        // Default timeout in milliseconds for client connections
  LPSECURITY_ATTRIBUTES lpSecurityAttributes // Optional security attributes (NULL for default)
);

This API call accepts an nMaxInstances argument. This parameter allows the first instance of the pipe to explicitly specify the maximum number of instances that can be created for the named pipe identified by lpName. As long as nMaxInstances falls within the range of 1 to PIPE_UNLIMITED_INSTANCES (which has a value of 255), multiple instances of the pipe can be created. This capability is essential for multithreaded named pipe servers or for handling overlapping I/O operations to serve simultaneous connections from multiple clients. However, this flexibility also introduces a significant security risk: it allows other processes, including malicious ones, to potentially hijack the named pipe.

Consider a scenario involving a high-privileged program that sets up both a named pipe server and a client for IPC. If a low-privileged attacker manages to create an instance of the named pipe server before the legitimate high-privileged program does, the attacker could potentially intercept messages from the legitimate client. Worse, if the client program relies on the server’s responses to execute critical actions, such as running commands or modifying system configurations, this interception could lead directly to a privilege escalation.

The order of creation is paramount in Windows named pipes because clients connect to server instances in first-in, first-out (FIFO) order. Additionally, for a named pipe to be susceptible to this type of hijacking, the dwOpenMode argument in the CreateNamedPipe call must not include the FILE_FLAG_FIRST_PIPE_INSTANCE (0x00080000) flag. This flag specifically prevents the creation of additional instances of a pipe if an instance already exists, thus acting as a safeguard against certain forms of pipe hijacking. This specific condition was central to CVE-2022-21893, a notable privilege escalation exploit found in Windows Remote Desktop Services (RDS) that allowed an attacker to intercept the messages exchanged via RDS named pipe IPC.

Security Misconfigurations in Named Pipes: ACL Weaknesses

Because Windows named pipes rely on developers to explicitly and correctly set an Access Control List (ACL) using the lpSecurityAttributes argument, rather than delegating access control to the default filesystem permissions, misconfigured access controls can lead to critical information leaks or pave the way for privilege escalation.

By default, if no specific lpSecurityAttributes are provided (or if they are configured insecurely), the named pipe can grant read access to members of the Everyone group and the anonymous account. If an unaware developer transmits sensitive data over such a weakly configured named pipe, a low-privileged attacker can effortlessly access and exfiltrate it. Furthermore, a misconfigured ACL could allow an attacker to establish a client connection to a privileged named pipe server and subsequently send arbitrary messages. If the server’s message handler uses this untrusted input to execute privileged actions, a significant security boundary is breached, potentially leading to full system compromise.

Let’s examine the public description for CVE-2022-24286, which vividly illustrates such a scenario:

Acer QuickAccess 2.01.300x before 2.01.3030 and 3.00.30xx before 3.00.3038 contains a local privilege escalation vulnerability. The user process communicates with a service of system authority through a named pipe. In this case, the Named Pipe is also given Read and Write rights to the general user. In addition, the service program does not verify the user when communicating. A thread may exist with a specific command. When the path of the program to be executed is sent, there is a local privilege escalation in which the service program executes the path with system privileges.

While the source code of Acer QuickAccess is not publicly available, this description strongly indicates that an instance of a misconfigured ACL for a named pipe directly contributed to a severe privilege escalation vulnerability. How a developer might inadvertently create a world-readable and world-writable named pipe like this in C#:

C#
using System;
using System.IO.Pipes;                     // For named pipes
using System.Security.AccessControl;       // For defining ACL (access control list)
using System.Security.Principal;           // For working with security identifiers (SIDs)

public class Program {
    static void Main(string[] args) {
        // Create a SecurityIdentifier representing "Everyone" (WorldSid)
        SecurityIdentifier securityIdentifier = new SecurityIdentifier(
            WellKnownSidType.WorldSid, null );

        // Create an access rule that allows ReadWrite access to "Everyone"
        PipeAccessRule pipeAccessRule = new PipeAccessRule(
            securityIdentifier,                  // SID for "Everyone"
            PipeAccessRights.ReadWrite,          // Allow both read and write access
            AccessControlType.Allow );           // This rule is an "Allow" type

        // Create a new PipeSecurity object to hold the ACL
        PipeSecurity pipeSecurity = new PipeSecurity();
        pipeSecurity.AddAccessRule(pipeAccessRule);  // Add our rule to the ACL

        // Create a named pipe server with the specified security (world-readable/writable)
        NamedPipeServerStream pipeServer = NamedPipeServerStreamAcl.Create(
            "worldRWPipe",                            // Name of the pipe
            PipeDirection.InOut,                      // Allow reading and writing
            NamedPipeServerStream.MaxAllowedServerInstances,  // Max allowed simultaneous instances
            PipeTransmissionMode.Byte,                // Data is transmitted as raw bytes
            PipeOptions.Asynchronous,                 // Allow asynchronous (non-blocking) operations
            0,                                        // Input buffer size (default)
            0,                                        // Output buffer size (default)
            pipeSecurity );                           // Apply the world-access ACL

        // Wait for a client to connect before proceeding
        pipeServer.WaitForConnection();

        // At this point, untrusted input could be read or written by anyone on the system
        // ⚠️ This is dangerous if not properly validated or sandboxed
        // Example: pipeServer.Read(...), pipeServer.Write(...), etc.
    }
}

A world-readable and -writable named pipe

In this C# example, the code explicitly creates a PipeSecurity object and adds a PipeAccessRule that grants ReadWrite permissions to WellKnownSidType.WorldSid (representing “Everyone”). This effectively makes the named pipe accessible to any user, setting the stage for the vulnerabilities described.

In Unix-like systems, the creation of named pipes is handled by the mkfifo API call. This function takes two primary arguments: the pathname for the pipe as the first argument, and the file permission mode as the second. Similar to other file creation APIs in Unix, the effective mode of the created named pipe is modified by the system’s umask (mode & ~umask). Subsequently, access to the named pipe is determined by the standard filesystem permissions, just like any other file. This highlights a key difference in access control paradigms between Unix and Windows IPC mechanisms.


Other IPC Methods: A Growing Landscape

The repertoire of Inter-Process Communication (IPC) methods is in a state of constant evolution, driven by the continuous addition of features to operating systems and third-party software. The following is a non-exhaustive list, intended to illustrate the diversity of these mechanisms:

Developers often employ these APIs in highly creative, and sometimes inadvertently insecure, ways. For example, I once analyzed an application that utilized the Windows SendMessage function. This function is typically designed to send simple, one-way messages between windows within the desktop user interface. However, this particular application was using it to pass complex serialized data structures. The application determined which window to send the message to using the FindWindow function, which accepts two arguments: lpClassName and lpWindowName. Crucially, because the application set lpClassName to NULL, FindWindow returned the first window whose title matched lpWindowName. This implementation is even more insecure than using named pipes because the specific window returned by FindWindow is not guaranteed to be in FIFO (first-in, first-out) order. This non-determinism allows a cunning attacker to potentially man-in-the-middle (MITM) any messages sent using this channel, intercepting and manipulating sensitive data.

It is paramount to remain vigilant and alert for potentially unorthodox IPC implementations. Given that the various IPC mechanisms share the fundamental purpose of exchanging messages between processes, they often exhibit similar patterns in code, such as client/server listeners. Recognizing these common patterns can greatly assist you in identifying the presence and nature of IPC. As an integral part of your attack surface mapping, always strive to comprehensively enumerate all IPC methods employed by the target software.

File Formats: The Hidden Language of Data

Almost every piece of software needs to handle files in some capacity. From simple newline-delimited configuration files to complex video clips, data is encoded in a vast array of formats. Much like network protocols, file formats necessitate that software parses specific data structures in a standardized manner to correctly interpret the contents. Unfortunately, developers sometimes make crucial mistakes during the implementation of these parsers, which can directly lead to exploitable vulnerabilities. Furthermore, some older or proprietary file formats may have been designed without adequate consideration for modern security concerns, compelling developers to patch critical security gaps after the fact.

Many widely adopted file formats are meticulously documented in RFCs (Requests for Comments), providing a reliable source of truth for their structure. However, proprietary or older formats may demand significantly more investigative effort and reverse engineering. Over time, you will develop an intuitive ability to recognize common types and components within file formats. For example, file formats are often broadly organized into three conceptual parts:

It’s important to recognize that there is a significant variance among file formats, and not all adhere rigidly to this header-body-footer paradigm. For instance, the XML (Extensible Markup Language) format is entirely markup-based. It relies on a predefined set of symbols (tags) that dictate how different parts of the file should be processed. Markup-based formats are prevalent in text documents, such as this very book, which I authored in LaTeX. For XML, the most critical symbols are the < and > characters, which delineate tags in an XML document:

<?xml version="1.0"?>
<greeting>Hello, world!</greeting>

In this simple XML example, there is no explicit evidence of a footer.

Other formats diverge even further from the classic header-body-footer pattern. Directory-based formats are a prime example; they organize data into multiple files structured within a logical directory hierarchy. A classic illustration of this is Microsoft Office documents (such as .docx for Word documents and .pptx for PowerPoint presentations). These files are, in essence, ZIP files in disguise. If you rename a .docx file to .zip, you can often open it with a standard file archiver like 7-Zip, revealing its internal structure of XML documents, images, and other resource files. Software like Microsoft Word differentiates a .docx file from a generic .zip file primarily via its filename extension. However, it’s not as simple as taking any random ZIP file, changing its extension to .docx, and expecting Microsoft Word to open it successfully. The DOCX format imposes additional, stringent requirements on top of the basic ZIP archive structure, concerning the mandatory existence and specific organization of files within the archive, as well as their precise contents.

Given the immense diversity of file formats, I will highlight some common patterns that typically warrant greater scrutiny from a security perspective.

Type–Length–Value (TLV): A Ubiquitous Pattern

The Type–Length–Value (TLV) pattern is a fundamental and widely used structure found in both network protocols and file formats. We often see TLV employed for chunked data within the body of a file or a protocol message because its self-describing structure allows a parser to easily identify and consume chunks of variable length. It consists of three distinct parts:

The popular Portable Network Graphics (PNG) format is an excellent real-world example of a file format that extensively uses the TLV pattern. The body of a PNG file is composed of a series of self-contained chunks, each of which is made up of four specific parts:

  1. Length (4 bytes): Specifies the size of the chunk data.
  2. Chunk Type (4 bytes): Identifies the type of data within the chunk (e.g., IHDR for header, IDAT for image data).
  3. Chunk Data (Length bytes): The actual payload data of the chunk.
  4. CRC (Cyclic Redundancy Check) Checksum (4 bytes): A checksum to ensure the integrity of the preceding Chunk Type and Chunk Data.

How the critical header chunk type, denoted by the IHDR chunk type code, is parsed:

An Example PNG IHDR Chunk

PartHex bytesValue
Length00 00 00 0d13
Type49 48 44 52IHDR
Data00 00 00 01Width: 1
Data00 00 00 01Height: 1
Data08Bit depth: 8
Data00Color type: 0
Data00Compression: 0
Data00Filter: 0
Data00Interlace: 0
CRC3a 7e 9b 55CRC-32: 3A7E9B55

Vulnerabilities in TLV Implementations

When implementing TLV parsing, developers sometimes make a critical oversight: they forget to adequately check for mismatches between the expected size of a chunk (as dictated by its Type definition) and the actual Length value provided in the TLV structure itself. For example, the IHDR chunk, as strictly defined by the PNG format specification, should always contain precisely 13 bytes’ worth of metadata (for width, height, bit depth, etc.). However, a careless developer might blindly trust the value provided by the Length part of the TLV structure. This trust could lead to a dangerous scenario where the parser attempts to copy an attacker-controlled Length number of bytes (which could be up to the maximum value of a 32-bit unsigned integer, 2,147,483,647) into a small, fixed-size 13-byte IHDR struct buffer. This is a classic buffer overflow vulnerability.

A real-world instance of such a vulnerability in Apache OpenOffice (CVE-2021-33035). This office suite application accepted the dBase database file (DBF) format. The DBF format includes a field descriptor array within its header, where each field descriptor defines a field type (1 byte) and a size (1 byte). Unfortunately, OpenOffice’s code mistakenly trusted both of these values. For a field type of I (corresponding to an integer), the code correctly allocated a buffer of 4 bytes (which is appropriate for an Int32 type). However, it then proceeded to copy the attacker-controlled size number of bytes into that 4-byte buffer, as seen in the code snippet:

C++
// nType is taken from field descriptor type value
else if ( DataType::INTEGER == nType )
{
    // sal_Int32 type is 4 bytes
    sal_Int32 nValue = 0;
    // nLen is taken from field descriptor size value
    memcpy(&nValue, pData, nLen);
    *(_rRow->get())[i] = nValue;
}

Since the size field in the field descriptor structure was only 1 byte, it had a maximum possible value of 255. This allowed an attacker to specify a size of up to 255, leading to an overflow of 251 bytes (255 - 4) that disastrously overwrote a return pointer address on the stack. This seemingly small discrepancy was sufficient to construct a full-blown code execution exploit, as detailed in my blog post, “All Your D-Base Are Belong To Us, Part 1: Code Execution in Apache OpenOffice.”

The Vulnerability Chain

  1. Size Field Limitation: The 1-byte size field could only hold values 0-255
  2. Buffer Allocation: The program likely allocated a buffer based on this size field
  3. Off-by-Four Error: When copying data, 4 bytes were subtracted (possibly for headers/offsets), resulting in a 251-byte overflow
  4. Stack Corruption: This overflow was enough to overwrite the return address on the stack

Why This Was Critical

Even though 251 bytes might seem small:

Key Security Lessons

  1. Input Validation: Always validate size fields against reasonable bounds
  2. Integer Bounds: Understand the limitations of your data types
  3. Buffer Calculations: Be extremely careful with arithmetic that affects memory operations
  4. Stack Protection: Modern mitigations like stack canaries, ASLR, and DEP help, but proper bounds checking is still essential

Vulnerable Code Example

C++
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// Field descriptor structure (simplified)
struct field_descriptor {
    char name[32];
    unsigned char size;        // ONLY 1 BYTE - max 255!
    char type;
};

void process_field_data(struct field_descriptor *desc, char *input_data) {
    // Allocate buffer based on the size field
    char *buffer = malloc(desc->size);

    // Process data - but subtract 4 bytes (simulating header removal)
    // This is where the bug occurs!
    int copy_size = desc->size - 4;  // INTEGER UNDERFLOW + BUFFER OVERFLOW

    printf("Allocated: %d bytes, Copying: %d bytes\n", desc->size, copy_size);

    // VULNERABLE: No bounds checking on copy_size
    memcpy(buffer, input_data, copy_size);  // BOOM!

    free(buffer);
}

int main() {
    struct field_descriptor desc;
    char large_input[500];  // Our attack payload

    // Set up malicious field descriptor
    strcpy(desc.name, "VulnerableField");
    desc.size = 255;        // Maximum value for unsigned char
    desc.type = 'S';

    // Fill our payload with recognizable data
    memset(large_input, 'A', sizeof(large_input));
    large_input[251] = 'R';  // 'R' for Return address overwrite
    large_input[252] = 'E';
    large_input[253] = 'T';
    large_input[254] = '!';  // End marker

    printf("Demonstrating the vulnerability...\n");
    printf("Size field: %d\n", desc.size);

    // This will cause the overflow
    process_field_data(&desc, large_input);

    return 0;
}

What Actually Happens

When desc.size = 255:

  1. Buffer Allocation: malloc(255) – allocates 255 bytes
  2. Copy Calculation: copy_size = 255 - 4 = 251
  3. Memory Copy: memcpy(buffer, input_data, 251)
  4. Overflow: 251 bytes copied into 255-byte buffer = 246 bytes of overflow data
  5. Stack Corruption: The extra 246 bytes overwrite stack variables, potentially including return addresses

Real-World Exploitation

In a real exploit scenario, an attacker would:

C++
// Example attack payload structure
char exploit_payload[255] = {
    // Useful data (first ~240 bytes)
    0x90, 0x90, 0x90, 0x90,     // NOP sled
    // ... shellcode ...

    // Overwrite return address (last 4 bytes)
    0x37, 0x12, 0x40, 0x00      // Address of shellcode or ROP gadgets
};

Memory Layout Visualization

C++
Stack Layout:
[Buffer: 255 bytes] [Saved EBP: 4 bytes] [Return Address: 4 bytes]
[Local vars...]     [Stack Canary...]    [Return Address to overwrite]

When copying 251 bytes:
[247 bytes payload] [4 bytes overwrite return address] [BOOM!]

The Fix

C++
void process_field_data_SAFE(struct field_descriptor *desc, char *input_data) {
    // Validate size first
    if (desc->size > 200) {  // Reasonable limit
        fprintf(stderr, "Size too large!\n");
        return;
    }

    char *buffer = malloc(desc->size);

    // Safe calculation with bounds checking
    int copy_size = desc->size - 4;
    if (copy_size > desc->size || copy_size < 0) {
        fprintf(stderr, "Invalid copy size!\n");
        free(buffer);
        return;
    }

    // Additional bounds checking
    if (copy_size > MAX_REASONABLE_SIZE) {
        fprintf(stderr, "Copy size too large!\n");
        free(buffer);
        return;
    }

    memcpy(buffer, input_data, copy_size);
    free(buffer);
}

This demonstrates how a simple 1-byte field limitation led to a critical buffer overflow that could be exploited for remote code execution. The key lesson: always validate input parameters that affect memory operations, regardless of how small the field might seem.

Many file formats and network protocols utilize the TLV pattern. When analyzing them, always make it a priority to test for vulnerabilities that arise from type and length discrepancies. These are fertile grounds for discovering critical security flaws.


Directory-Based File Formats: Layers of Vulnerability

A significant subset of file formats are directory-based, meaning that the “file” you interact with is actually a wrapper or container around a collection of other files and a defined directory structure. Typically, directory-based formats necessitate a manifest file (often an XML document or similar structured data) that contains crucial additional metadata about the rest of the files within the container, including their names, types, and relative locations. This pattern tends to expose two primary types of vulnerabilities: those related to file traversal and those stemming from child format parsing.

File Traversal: Insecure Parsing of Relative Paths

File traversal vulnerabilities occur when software insecurely parses the directory data within these container formats, particularly relative paths. Let’s consider the ubiquitous ZIP format, upon which many directory-based formats are ultimately built. A ZIP archive is generally structured like this:

[local file header 1]
[local file header 1]
[file data 1]
[data descriptor 1]
. . .
[local file header n]
[file data n]
[data descriptor n]
[archive decryption header]
[archive extra data record]
[central directory]
[zip64 end of central directory record]
[zip64 end of central directory locator]
[end of central directory record]

Each file header (which appears in both the “local file headers” scattered throughout the archive and the centralized “central directory” structure) contains metadata about a specific file or directory contained within the archive. Critically, this header includes a filename field of variable size. The filename can, by design, include a relative path; for instance, a filename like nested/file would instruct an extractor to place it at ./nested/file within the output directory.

However, a dangerous omission often lies in the lack of explicit restrictions on filenames that incorporate path traversal values, such as ../../../../tmp/file. A parser that blindly trusts such a value, without proper sanitization or validation, could extract files into highly sensitive or dangerous locations, such as system cron job folders (where executable scripts are stored for scheduled execution) or critical application working directories. When reviewing code related to directory-based formats, it is paramount to pay close attention to how the software handles data pertaining to the locations of the files within the archive. Look for any instances where filename or path components are used directly in file system operations without rigorous checks for ../ or absolute path indicators.

Child Format Vulnerabilities: The Nested Threat

Next, consider the types of individual files contained within the directory-based format. For instance, many directory-based formats employ an XML file as their manifest, which holds vital information about how to parse and utilize the remaining files in the package. Consequently, any software designed to handle these container files must first parse the XML manifest.

The XML format itself has a number of potential vulnerabilities if parsed insecurely, with XML External Entity (XXE) injection being a notorious example. In brief, the XML standard allows for the inclusion of external entities, which can point to resources both local (e.g., files on the system) and remote (e.g., URLs). By carefully crafting a malicious XML file to leverage these external entities, an attacker can force a vulnerable XML parser to disclose sensitive local file data to a remote address controlled by the attacker.

This was precisely the case with CVE-2022-0219, an XXE injection vulnerability discovered in JADX, a widely used open-source Android application decompiler. Android applications are typically distributed in the Android Package (APK) format, which fundamentally is a directory-based format that must include an AndroidManifest.xml manifest file. By embedding an XXE payload directly into the AndroidManifest.xml, an attacker could coerce JADX into disclosing sensitive local file data when attempting to export a decompiled Android application. To remediate this, JADX wisely switched to a more secure XML parser that was configured not to process external entities, effectively mitigating the XXE risk.

Child format-related vulnerabilities often arise because developers tend to focus their security efforts primarily on the parent directory-based format’s parsing logic, while inadvertently delegating the handling of child file formats to external libraries. These external libraries, if not configured or used correctly, may not parse securely by default, opening new attack vectors. Therefore, when conducting security reviews, look for instances where child files are processed, particularly manifests, and thoroughly validate their usage and the parsing libraries involved.

Sometimes, both types of vulnerabilities (file traversal and child format issues) can converge within the same software. I personally encountered this in a custom package format that was built upon ZIP and utilized an XML manifest. By cleverly chaining a ZIP path traversal vulnerability with an XXE injection, I was able to enumerate the target filesystem and ultimately upload a web shell, achieving full remote code execution. The details of this exploit are documented in my post, “A Tale of Two Formats: Exploiting Insecure XML and ZIP File Parsers to Create a RCE.”

Custom Fields: Uncharted Territory

File formats often incorporate reserved bytes or extendable fields that allow developers to add custom functionality beyond the standard specification. These custom functionalities are frequently poorly documented and can introduce unexpected features, making them particularly dangerous from a security perspective.

Consider the iCalendar (ICS) format, which is used by nearly all calendar software, from Microsoft Outlook to Apple Calendar. The ICS format provides a “standard mechanism for doing non-standard things” through nonstandard properties denoted by an X- prefix (e.g., X-MY-CUSTOM-PROPERTY). This flexibility has historically led to all sorts of interesting behaviors that extended far beyond the default ICS properties like event location, time, and name. For example, older versions of Microsoft Office supported a property called X-MS-OLK-COLLABORATEDOC. This property would automatically open a conferencing collaboration document when an event started. Given that calendar events can be created and sent remotely via event invitations, this could lead to extremely dangerous outcomes, such as forcing a user to automatically open a malicious file from a network share without their explicit consent.

Another common scenario arises when developers “jerry-rig” custom fields by parsing data differently from how a standard explicitly defines it. Take the HTML format, which defines the <link> element. This element specifies external resources related to the current HTML document, with the type of relationship denoted by the rel attribute. Thus, to indicate a stylesheet located at main.css, an HTML document might include the following element:

HTML
<link href="main.css" rel="stylesheet">

The HTML standard defines a specific list of supported tokens for the rel attribute and specifies their expected behavior. However, the WeasyPrint HTML-to-PDF conversion engine demonstrates how this functionality can be extended. WeasyPrint supports a custom attachment value for rel that does not appear in the official HTML standard. By using this custom value, a developer can include local files as attachments to the generated PDF output:

HTML
<link href="file:///etc/passwd" rel="attachment">

It’s crucial to understand that, in this specific example, WeasyPrint’s support for rel="attachment" is a feature, not a vulnerability in itself. However, a developer who uses WeasyPrint in their software without adequately accounting for this extended behavior could inadvertently introduce a significant vulnerability into their own application (e.g., allowing an attacker to specify arbitrary local files to be attached to a PDF, leading to information disclosure).

To identify these kinds of custom implementations, meticulously look for ways in which the code diverges from a file format’s specification, going beyond just typical implementation errors. While established standards often undergo a rigorous, open vetting process that considers various security issues, custom extensions may not receive such intense scrutiny and can, unfortunately, repeat common security mistakes.


WeasyPrint: A Feature That Can Become a Vulnerability

WeasyPrint is a Python library that converts HTML and CSS to PDF. One of its features is that it can:

👉 This behavior is documented and intentional — it’s a feature, not a bug.

However, if a developer allows user-controlled HTML input to be passed directly into WeasyPrint without sandboxing, an attacker could:

  <img src="file:///etc/passwd" />
  <link rel="stylesheet" href="http://169.254.169.254/latest/meta-data">

So while WeasyPrint itself is not inherently vulnerable, its powerful features can introduce vulnerabilities if used carelessly in a web application context.

This is a classic example of a “secure component used insecurely.”


Custom Extensions vs. Standards: The Security Implication

“Look for ways in which the code diverges from a file format’s specification beyond just implementation errors.”

This is a key red flag during security reviews.

Why Standards Matter:

Risks of Custom Behavior:

When developers extend or modify behavior beyond the spec — especially to add “convenience” — they may unknowingly:

  1. Bypass security boundaries:
  1. Introduce parser confusion:
  1. Repeat known mistakes:

Example: A “custom template engine” that allows {{ include('/etc/passwd') }} is not a flaw in the engine — it’s a design decision that introduces risk.


How to Identify Risky Custom Implementations

During code review or threat modeling, ask:

QuestionPurpose
Does this code process untrusted input using a non-standard parser or loader?Custom parsers often lack security hardening.
Are there extensions to HTML/CSS/URL handling not in the official spec?Could enable file access, SSRF, or injection.
Is the feature surface larger than necessary?Attack surface grows with features like local file access.
Is there sandboxing or input validation for resource loading?Missing isolation is a red flag.
Are external or local resources fetched without user consent or limits?Risk of SSRF, data leakage, or DoS.

Best Practices

  1. Treat powerful features as dangerous by default
    → Assume any feature that reads files, makes HTTP requests, or executes logic is a potential attack vector.
  2. Sandbox untrusted input
    → Strip or rewrite file://, javascript:, or data: URIs before passing to WeasyPrint.
    → Run conversions in isolated environments (containers, chroot, etc.).
  3. Follow the principle of least privilege
    → Run the service with minimal file system access.
    → Disable networking if not needed.
  4. Avoid custom parsing/formatting logic
    → Use standard-compliant tools when possible.
    → If extending, document and audit the security implications.
  5. Validate and sanitize inputs
    → Use allowlists for supported HTML/CSS.
    → Reject or rewrite dangerous constructs.

Conclusion

We have embarked on a comprehensive exploration of a diverse range of potential attack vectors that extend far beyond traditional web applications. You’ve learned how to systematically identify the source code that defines and exposes these attack vectors, from intricate network protocols to nuanced inter-process communication (IPC) mechanisms. We’ve delved into common patterns found in file formats and examined the vulnerabilities frequently associated with them, equipping you with a broader perspective on software security.

Ultimately, the precise attack surface of any given software application can vary dramatically based on its specific threat model and the environment in which it operates. For instance, a local attacker on a Windows system might successfully exploit IPC mechanisms like window messages or named pipes, whereas a remote attacker would be limited to accessing only exposed network protocols and network-enabled IPC mechanisms, such as named pipes configured for network access.

Whether a particular attack vector is truly “viable” largely depends on whether it can be leveraged to cross a security boundary. As you meticulously enumerate the attack surface of software from its source code, always use this critical distinction to correctly identify potential vulnerabilities and quickly focus your efforts on the most exploitable scenarios.

By diligently applying the various techniques outlined in this chapter, you will be significantly better equipped to accurately assess an application’s attack surface and construct a realistic threat model before delving into the intricate depths of code review. As you prepare to expand into larger-scale variant analysis in the next chapter, the ability to narrow your search space to genuinely reachable attack surfaces will prove absolutely critical to the accuracy and efficiency of your results.

Exit mobile version