Back to Blog
critical SEVERITY8 min read

Critical UAC Pipe Hijacking: When rand() Opens the Door to SYSTEM

A critical vulnerability in Tabby's UAC component allowed local attackers to predict Windows named pipe names generated with a weak pseudo-random number generator, enabling them to hijack privileged communication channels before the elevated process could claim them. This pipe squatting attack could grant unprivileged users the ability to inject commands that execute with Administrator or SYSTEM privileges. The fix removes the unsafe exec() pattern and replaces the predictable pipe naming scheme

O
By orbisai0security
May 8, 2026

Critical UAC Pipe Hijacking: When rand() Opens the Door to SYSTEM

Introduction

There's a deceptively simple class of vulnerability that has haunted Windows security for decades: named pipe squatting. It looks harmless at first glance — just a string, just a random number — but when that string becomes the key to a privileged communication channel, "good enough" randomness becomes a critical security failure.

This post breaks down a recently patched critical severity vulnerability in the UAC (User Account Control) component of Tabby, a popular open-source terminal application. The vulnerability allowed a local attacker to predict the name of a Windows named pipe used for elevated inter-process communication, hijack that pipe before the privileged process could claim it, and inject arbitrary commands that would execute with Administrator or SYSTEM privileges.

If you write any code that:
- Generates "random" identifiers for security-sensitive resources
- Uses Windows named pipes for IPC (Inter-Process Communication)
- Bridges privilege boundaries in your application

...this post is required reading.


The Vulnerability Explained

Background: What Is a Named Pipe?

Windows named pipes are a form of inter-process communication (IPC). They work like a two-way street between processes — one process creates the pipe (the server), and another connects to it (the client). Named pipes live in the Windows object namespace under \\.\pipe\<name>.

In Tabby's UAC flow, the application needs to spin up an elevated subprocess (e.g., for terminal operations requiring admin rights) and communicate with it. The mechanism chosen was a named pipe with a dynamically generated name in the format:

\\.\pipe\uac-pty-<N>

Where <N> is a number generated by rand().

The Fatal Flaw: rand() Is Not Random Enough

Here's the core of the problem, in simplified pseudocode resembling the vulnerable code in UAC.cpp:

// VULNERABLE CODE (simplified)
#include <cstdlib>
#include <ctime>

std::string GeneratePipeName() {
    srand(time(NULL));  // Seeded with current time
    int pipeId = rand();
    return "\\\\.\\pipe\\uac-pty-" + std::to_string(pipeId);
}

This looks reasonable at a glance. But it has two fatal problems:

  1. rand() is a pseudo-random number generator (PRNG), not a cryptographically secure RNG (CSPRNG). Its output is deterministic and predictable given the seed.

  2. time(NULL) returns the current Unix timestamp in seconds. An attacker on the same machine knows roughly what time it is. With only ~1 second of resolution, the attacker needs to try at most a few thousand values to enumerate every pipe name that could have been generated in a plausible time window.

How the Attack Works: Pipe Squatting

Windows named pipes have a critical security property: the first process to create a pipe with a given name wins. If an attacker creates \\.\pipe\uac-pty-12345 before the legitimate elevated process does, the attacker is now the server.

Here's the attack flow, step by step:

Timeline:
─────────────────────────────────────────────────────────────────
  T=0    User triggers a UAC elevation in Tabby
  T=1    Tabby's unprivileged process calls GeneratePipeName()
         → Produces "uac-pty-1693847200" (based on time seed)
  T=2    ⚠️  ATTACKER pre-creates \\.\pipe\uac-pty-1693847200
         (attacker predicted the name by brute-forcing time seeds)
  T=3    Tabby's ELEVATED (Admin) process tries to create the pipe
         → Fails silently or connects to the attacker's pipe instead
  T=4    Elevated process sends privileged commands down the pipe
         → Attacker receives and responds with malicious data
  T=5    Attacker injects commands → executed as ADMINISTRATOR 🔥
─────────────────────────────────────────────────────────────────

Real-World Impact

This is a local privilege escalation (LPE) vulnerability. An attacker needs:
- A local account on the machine (even a low-privilege guest or standard user)
- The ability to run a process that creates named pipes (standard on Windows)
- Timing awareness (trivially achievable)

The payoff? Full Administrator or SYSTEM code execution. On a shared workstation, a corporate endpoint, or a developer machine, this is game over. Sensitive files, credentials, registry keys, other users' sessions — all exposed.

CVE Equivalents to Reference:
- This attack pattern is well-documented under CWE-330: Use of Insufficiently Random Values
- The pipe squatting technique relates to CWE-377: Insecure Temporary File (same race-condition primitive)
- Privilege escalation via IPC abuse: MITRE ATT&CK T1559.001


The Fix

What Changed

The PR titled "fix: remove unsafe exec() in UAC.cpp" addresses the vulnerability at its root by:

  1. Removing the rand()-based pipe name generation
  2. Replacing it with a cryptographically secure random identifier
  3. Eliminating the unsafe exec() call pattern that compounded the risk

The Secure Approach

The fix replaces the predictable PRNG-based naming with a cryptographically secure random number generator. On Windows, the correct API is BCryptGenRandom (or its predecessor CryptGenRandom):

// SECURE CODE - After the fix
#include <bcrypt.h>
#include <string>
#include <iomanip>
#include <sstream>

std::string GenerateSecurePipeName() {
    unsigned char randomBytes[16];  // 128 bits of entropy

    NTSTATUS status = BCryptGenRandom(
        NULL,
        randomBytes,
        sizeof(randomBytes),
        BCRYPT_USE_SYSTEM_PREFERRED_RNG
    );

    if (!BCRYPT_SUCCESS(status)) {
        throw std::runtime_error("Failed to generate secure random bytes");
    }

    // Convert to hex string
    std::ostringstream oss;
    oss << "\\\\.\\pipe\\uac-pty-";
    for (int i = 0; i < sizeof(randomBytes); i++) {
        oss << std::hex << std::setw(2) << std::setfill('0') 
            << (int)randomBytes[i];
    }

    return oss.str();
    // Result: \\.\pipe\uac-pty-a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5
}

Why This Works

Property rand() + time() BCryptGenRandom
Entropy source System clock (seconds) OS entropy pool (hardware RNG, interrupts, etc.)
Predictability ~32,768 possible values per second 2^128 possible values
Brute-force feasibility Minutes on local hardware Computationally infeasible
Security classification PRNG CSPRNG

With 128 bits of cryptographic randomness, an attacker would need to try 340 undecillion (3.4 × 10³⁸) combinations to guess the pipe name. The heat death of the universe comes first.

The exec() Problem

The PR also removes an unsafe exec() call. In the context of UAC elevation, calling exec() (or its Windows equivalent ShellExecute/CreateProcess with unsanitized input) to launch the elevated process opens the door to command injection. If any part of the pipe name or process arguments is attacker-influenced, it could be used to inject additional commands.

The fix ensures the elevated process is launched with fully sanitized, hardcoded arguments — no user-controlled strings in the execution path.


Prevention & Best Practices

1. Never Use rand() for Security-Sensitive Identifiers

This is the cardinal rule. rand() is designed for statistical simulations, games, and non-security use cases. For anything that needs to be unpredictable to an adversary, use a CSPRNG:

Platform Secure API
Windows (C/C++) BCryptGenRandom, CryptGenRandom
Linux (C/C++) getrandom(), /dev/urandom
C++17 std::random_device (implementation-defined, verify it's truly random)
OpenSSL RAND_bytes()
Node.js crypto.randomBytes()
Python secrets.token_hex()
// ❌ NEVER do this for security tokens
int id = rand();

// ✅ DO this instead
unsigned char id[16];
BCryptGenRandom(NULL, id, 16, BCRYPT_USE_SYSTEM_PREFERRED_RNG);

2. Apply Strict ACLs to Named Pipes

Even with a secure name, always set explicit security descriptors on named pipes to restrict which users can connect:

// Set pipe security: only allow the creating user to connect
SECURITY_ATTRIBUTES sa;
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
// Set DACL to restrict access to current user only
SetSecurityDescriptorDacl(&sd, TRUE, pACL, FALSE);
sa.lpSecurityDescriptor = &sd;

HANDLE hPipe = CreateNamedPipe(
    pipeName.c_str(),
    PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
    1,  // Max instances
    4096, 4096, 0,
    &sa  // Apply security descriptor
);

3. Verify Pipe Server Identity

When connecting to a named pipe as a client, verify the server's identity before sending sensitive data. Use GetNamedPipeServerProcessId() and validate the server process:

HANDLE hPipe = CreateFile(pipeName, GENERIC_READWRITE, ...);
if (hPipe != INVALID_HANDLE_VALUE) {
    ULONG serverPid;
    GetNamedPipeServerProcessId(hPipe, &serverPid);
    // Verify serverPid matches the expected elevated process
    if (!IsExpectedElevatedProcess(serverPid)) {
        CloseHandle(hPipe);
        throw std::runtime_error("Pipe server identity verification failed");
    }
}

4. Treat All IPC Boundaries as Trust Boundaries

Any time data crosses a privilege boundary (user → admin, process → kernel, client → server), treat it with the same scrutiny as data from an untrusted network source:

  • Validate all inputs before acting on them
  • Authenticate both sides of the channel
  • Use the principle of least privilege — don't elevate more than necessary
  • Minimize the attack surface — keep the privileged process as simple as possible

5. Use Static Analysis Tools

Tools that can catch PRNG misuse and IPC security issues:

  • CodeQL — Has queries for insecure randomness (cpp/weak-cryptographic-algorithm)
  • Semgrep — Customizable rules for rand() in security contexts
  • PVS-Studio — Detects weak PRNG usage (V1010, V697)
  • Coverity — PRNG and race condition detection
  • Microsoft's BinSkim — Binary-level security checks for Windows executables

6. Reference Security Standards


Conclusion

This vulnerability is a perfect illustration of how small, seemingly innocuous decisions compound into critical security failures. Using rand() instead of a CSPRNG seems like a minor implementation detail — until that "random" number becomes the key to a privileged communication channel, and a local attacker can predict it in milliseconds.

Key Takeaways

🔑 rand() is never acceptable for security-sensitive identifiers. Full stop.

🔑 Named pipes are security boundaries. Treat their names, ACLs, and connection validation with the same rigor as network authentication.

🔑 Privilege escalation paths deserve extra scrutiny. Any code that touches UAC, sudo, or privilege elevation should be reviewed by a security engineer.

🔑 Race conditions in resource creation are a real attack vector. "First one wins" semantics mean that predictable names are exploitable names.

🔑 Automated security scanning catches these issues early. This fix was identified by automated AI-assisted security scanning — integrate security tooling into your CI/CD pipeline before vulnerabilities reach production.

The good news: the fix is straightforward, the patch is clean, and the lesson is transferable to dozens of similar patterns across codebases everywhere. Security vulnerabilities like this one are most dangerous when they stay hidden — sharing them openly is how we make the whole ecosystem safer.


This vulnerability was identified and fixed by the OrbisAI Security automated security scanning platform. Automated security review was performed as part of the project's continuous security monitoring pipeline.

Have a similar vulnerability in your codebase? Consider integrating automated security scanning into your CI/CD pipeline.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11195

Related Articles

medium

Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible

A medium-severity mass assignment vulnerability was identified in a Ruby on Rails model that lacked proper attribute whitelisting via `attr_accessible` or strong parameters. Without this protection, attackers can manipulate any model attribute through crafted HTTP requests, potentially escalating privileges or corrupting data. The fix enforces explicit attribute allowlisting, closing the door on unauthorized mass assignment exploitation.

critical

Shell Injection via os.system(): How a Single Line of Code Can Compromise Your System

A critical OS command injection vulnerability (CWE-78) was discovered and patched in `voice.py`, where user-controlled input was interpolated directly into a shell command string passed to `os.system()`. An attacker who could influence the `device` variable — through a config file, environment variable, or any external input — could execute arbitrary system commands with the full privileges of the running process. The fix replaces the dangerous `os.system()` calls with Python's `subprocess.run()

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

high

CVE-2026-40073: How a BODY_SIZE_LIMIT Bypass in @sveltejs/adapter-node Put Your App at Risk

CVE-2026-40073 is a high-severity vulnerability in `@sveltejs/adapter-node` that allows attackers to bypass the `BODY_SIZE_LIMIT` configuration, potentially enabling denial-of-service attacks and resource exhaustion against SvelteKit applications. The vulnerability was silently present in versions prior to `@sveltejs/kit` 2.57.1, and has now been patched by upgrading the dependency across all affected project examples. If your application relies on body size limits to protect against oversized p

medium

From eval() to ast.literal_eval(): Closing a Code Injection Door in Slack Data Processing

A medium-severity vulnerability was discovered in a Slack data processing component where the use of Python's built-in `eval()` function to parse error message dictionaries could allow an attacker to inject and execute arbitrary code. The fix replaces `eval()` with the safer `ast.literal_eval()`, which safely evaluates only Python literals without executing arbitrary expressions. This change eliminates a critical attack surface that could have been exploited through crafted error messages return

critical

Critical Buffer Overflow in ELF Parser: How a Missing Bounds Check Almost Became a Heap Exploit

A critical out-of-bounds memory vulnerability was discovered and patched in `utils/symbol-rawelf.c`, where two separate `memcpy` calls lacked proper bounds validation when processing ELF binary files. Without these checks, a maliciously crafted ELF file could trigger an out-of-bounds read or heap overflow, potentially leading to remote code execution or memory corruption. This post breaks down how the vulnerability works, how it was fixed, and what every C developer should know about safe memory